From edacb17171478be7b512b61deac520c394e34b3f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Jan 2025 16:12:53 +0100 Subject: [PATCH 01/60] 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 8f8abeb292f1..a2bc518ae3fd 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -280,9 +280,6 @@ def set_rules(self): 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 02/60] 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 14bf5d8a18a4..d2ea82803704 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 03/60] 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 3ea47e40ebba..0254d46e934e 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -5,8 +5,10 @@ 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 @@ def __init__(self, server_address, password): } 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 @@ def __init__(self, server_address, password): 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 @@ def on_package(self, cmd: str, args: dict): 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 @@ def on_package(self, cmd: str, args: dict): 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 @@ def on_package(self, cmd: str, args: dict): 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 @@ async def checkLevels(self): 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 @@ async def verifyChests(self): 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 @@ async def verifyLevel(self): 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 @@ async def verifyItems(self): 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 @@ async def verifyItems(self): 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 @@ async def verifyItems(self): # 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 @@ async def verifyItems(self): 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 7fc2ad8a873f..e6e8a7b2f663 100644 --- a/worlds/kh2/Regions.py +++ b/worlds/kh2/Regions.py @@ -540,7 +540,7 @@ 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 0f26b56d0e54..767c5643417e 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -194,8 +194,8 @@ def __init__(self, kh2world: KH2World) -> None: 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 @@ def get_sephiroth_rules(self, state: CollectionState) -> bool: # 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 cb80ec609887..bee60bd36b18 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 04/60] 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 72161dfbdebc..88362c8fa6d4 100644 --- a/test/general/test_entrances.py +++ b/test/general/test_entrances.py @@ -34,3 +34,30 @@ def get_entrance_name_to_source_and_target_dict(world: World): 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 386e0b0e9e11..f13178c6c519 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 05/60] 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 23b34907071c..167c6c2faa24 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 06/60] 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 93ec5640e0c2..8f5df8896ac9 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 07/60] 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 d1b274c19ae7..41c83db41995 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -87,7 +87,7 @@ def __repr__(self): 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 @@ def __call__(self, path: str) -> bool: 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 08/60] 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 473c189166be..2bae8e00ed34 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -33,6 +33,18 @@ "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 8fcc74d1c34a..cf0c692d06d8 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 09/60] 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 087e17c3e473..ed2923037eee 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 @@ def remove(self, state: CollectionState, item: Item) -> bool: 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 10/60] 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 729bfd441172..846650c68fef 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -48,6 +48,7 @@ class TunicItemData(NamedTuple): "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 11/60] 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 ed2923037eee..e86f731381e5 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -242,10 +242,18 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: 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 @@ def create_items(self) -> None: 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 @@ def remove_filler(amount: int) -> None: 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 12/60] 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 653c2ecaabb1..9e0868b0f4a8 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 13/60] 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 5c6203fd5761..85f2cf1861a7 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -245,7 +245,7 @@ def pre_fill(self) -> None: 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 14/60] 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 ddc70c3dd748..9058fc30619c 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 15/60] 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 afa67f233c69..d54e0fc64d46 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 @@ def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, s 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 16/60] 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 f6b2623f8c02..996ba3300575 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -709,8 +709,16 @@ def handle_connection_loss(self, msg: str) -> None: 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 17/60] 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 0fa75e1b8bfa..4848cd992664 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 @@ class FactorioOptions(PerGameCommonOptions): ArtilleryTrapCount, AtomicRocketTrapCount, AtomicCliffRemoverTrapCount, + InventorySpillTrapCount, ], start_collapsed=True ), diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a2bc518ae3fd..ca9f12f1b21a 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -78,6 +78,7 @@ class FactorioItem(Item): 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 @@ def create_regions(self): 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 @@ def sorter(loc: FactorioScienceLocation): 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 517a54e3d642..edec5b7acdc0 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 87669beaf199..07fd4c04afae 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 18/60] 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 a08e99257e17..828740859e9b 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 19/60] 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 7b62b9ef73b1..50d6279179d9 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -22,7 +22,7 @@ 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 cf0c692d06d8..32644d52e0b6 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -4,7 +4,7 @@ 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 20/60] 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 2bae8e00ed34..49ce147041ee 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -34,6 +34,11 @@ } 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 @@ "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 32644d52e0b6..29929bd67237 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 21/60] 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 0fdd62365a5a..8a973a0d7229 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 22/60] 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 0dd874b25029..8d33d7090044 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 23/60] 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 e6e8a7b2f663..72b3c95b0947 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 24/60] 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 0254d46e934e..a21c8c7c5536 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -836,12 +836,12 @@ def get_addresses(self): 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 25/60] 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 2d90395cacda..f33334898223 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 7e2ce1f3c752..7e03edf3a23b 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 26/60] 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 89f82870e4a7..02592cc3834a 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -7,7 +7,9 @@ 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 @@ def test_given_single_mods_when_generate_then_basic_checks(self): 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 27/60] 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 9e0868b0f4a8..51b72c93ad3d 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -28,9 +28,11 @@ 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 @@ async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint] 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 @@ async def disconnect(self, endpoint: Client): 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 @@ def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = Fal 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 d58bbe81e304..5bcc583c53b6 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -5,7 +5,8 @@ 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 @@ def _object_hook(o: typing.Any) -> typing.Any: 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 e32c266ffb67..2eb3b0d6f3c2 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 28/60] 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 987d19acf35f..a4b2f1bd25df 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 @@ def test_leak(self) -> None: """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 29/60] 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 7a21b19ef247..2263478ff5e2 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -136,6 +136,12 @@ def create_items(self): 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 @@ def generate_basic(self): 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 30/60] 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 02592cc3834a..1dd2ab4902f7 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 @@ def test_mod_entrance_randomization(self): 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 31/60] 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 43883b86f8ac..66a9157b3437 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 32/60] 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 000000000000..a140f5abae14 --- /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 33/60] 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 2eb3b0d6f3c2..e5d3b7e6c26a 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 34/60] 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 12dace632c8a..cc808c2732df 100644 --- a/settings.py +++ b/settings.py @@ -109,7 +109,7 @@ def changed(self) -> bool: 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 @@ def dump(self, f: TextIO, level: int = 0) -> None: # 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 35/60] 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 d9122d444c97..49e82069ee8d 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 36/60] 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 7b1a35666ae7..a887638e377a 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -9,7 +9,7 @@ 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 @@ def pre_fill(self) -> None: # 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 @@ def pre_fill(self) -> None: 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 @@ def priority(item): 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 37/60] 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 | 623 +++++++++--------- 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, 396 insertions(+), 374 deletions(-) diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index ef842263ad2c..e2d49e64ae14 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 @@ def create_region(name: str, exits: Iterable[str]) -> Region: 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 @@ def get_filler_item_rules(self): 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 8068ff17ac83..225fb4feab1b 100644 --- a/worlds/stardew_valley/bundles/bundle_room.py +++ b/worlds/stardew_valley/bundles/bundle_room.py @@ -4,7 +4,7 @@ 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 a68d4ae9c097..12b3e3558a67 100644 --- a/worlds/stardew_valley/content/mods/sve.py +++ b/worlds/stardew_valley/content/mods/sve.py @@ -10,15 +10,14 @@ 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 @@ def harvest_source_hook(self, content: StardewContent): 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 @@ def harvest_source_hook(self, content: StardewContent): 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 @@ def harvest_source_hook(self, content: StardewContent): 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 @@ def harvest_source_hook(self, content: StardewContent): 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 d859d3b16ff7..e5f67c431953 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.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 8b2e189c796e..75f0f75a23d2 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -10,7 +10,7 @@ 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 @@ 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 1bb4b2bea73b..de371b7c3a9b 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -14,7 +14,7 @@ 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 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, 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 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, 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 3123bb924307..667227cb9e2b 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 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, 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 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, 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 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, 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 ead4d62f1650..bc8c09ee9241 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -106,7 +106,7 @@ def __init__(self, skill: str): 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 add99a2c2e7e..2038d995a720 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.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 @@ def __init__(self, *args, **kwargs): 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 dc5deda427f3..5b117de68cf2 100644 --- a/worlds/stardew_valley/logic/action_logic.py +++ b/worlds/stardew_valley/logic/action_logic.py @@ -6,7 +6,6 @@ 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 bc2f6cb1263d..6d0cd11baf71 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.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 @@ def __init__(self, *args, **kwargs): 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 ef5eab0134d1..12e824d21295 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -2,7 +2,6 @@ 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.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 1aa71404ae51..2ff74523940e 100644 --- a/worlds/stardew_valley/mods/logic/quests_logic.py +++ b/worlds/stardew_valley/mods/logic/quests_logic.py @@ -3,8 +3,8 @@ 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.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 @@ def __init__(self, *args, **kwargs): 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 d59439a4879d..7a680d5faad0 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -7,7 +7,7 @@ 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 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: 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(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.professor_snail_cave, is_ginger_island=True), + RegionData(RegionName.coop), + RegionData(RegionName.barn), + RegionData(RegionName.shed), + RegionData(RegionName.slime_hutch), + + 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 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: # 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 ae8f7f8d5503..5c2e6a57a4db 100644 --- a/worlds/stardew_valley/scripts/update_data.py +++ b/worlds/stardew_valley/scripts/update_data.py @@ -12,7 +12,7 @@ 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 @@ def write_location_csv(locations: List[LocationData]): 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 af4c3c35330d..ff1fbba37648 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -6,7 +6,7 @@ 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 @@ def __or__(self, other): 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 @@ def __and__(self, other): 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 2f2092fdf7b6..d8db616f66f4 100644 --- a/worlds/stardew_valley/test/TestMultiplePlayers.py +++ b/worlds/stardew_valley/test/TestMultiplePlayers.py @@ -53,8 +53,6 @@ def test_different_money_settings(self): 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 c1e8c2c8f095..da17d749eaed 100644 --- a/worlds/stardew_valley/test/TestWalnutsanity.py +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -25,7 +25,7 @@ def test_logic_received_walnuts(self): 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 @@ def test_logic_received_walnuts(self): # 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 @@ def test_logic_received_walnuts(self): 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 04a1528dd8b1..513bb951e933 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 38/60] 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 f47e45b93c07..6718e48bee33 100644 --- a/kvui.py +++ b/kvui.py @@ -26,6 +26,10 @@ 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") From f28aff6f9a86b6adff6f67253d17ee12a5c49c92 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sun, 2 Feb 2025 14:25:34 +0000 Subject: [PATCH 39/60] Core: Replace generator creation/iteration in CollectionState methods (#4587) * Core: Replace generator creation/iteration in CollectionState methods Using generators in these functions incurs overhead to create the new generator instance, call the `any`/`all`/`sum` function and have the `any`/`all`/`sum` function iterate the generator, which in turn iterates the iterable. Replacing the use of generators with for loops is faster. Getting `self.prog_items[player]` once in advance also improves performance of iterating longer iterables. * Add comment on the choice of for loops instead of any()/all()/sum() --- BaseClasses.py | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e19ba5f7772e..3d0004806cc5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -869,21 +869,40 @@ def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) def has(self, item: str, player: int, count: int = 1) -> bool: return self.prog_items[player][item] >= count + # for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of + # creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the + # argument to all() would be a new generator instance, for example. def has_all(self, items: Iterable[str], player: int) -> bool: """Returns True if each item name of items is in state at least once.""" - return all(self.prog_items[player][item] for item in items) + player_prog_items = self.prog_items[player] + for item in items: + if not player_prog_items[item]: + return False + return True def has_any(self, items: Iterable[str], player: int) -> bool: """Returns True if at least one item name of items is in state at least once.""" - return any(self.prog_items[player][item] for item in items) + player_prog_items = self.prog_items[player] + for item in items: + if player_prog_items[item]: + return True + return False def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool: """Returns True if each item name is in the state at least as many times as specified.""" - return all(self.prog_items[player][item] >= count for item, count in item_counts.items()) + player_prog_items = self.prog_items[player] + for item, count in item_counts.items(): + if player_prog_items[item] < count: + return False + return True def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool: """Returns True if at least one item name is in the state at least as many times as specified.""" - return any(self.prog_items[player][item] >= count for item, count in item_counts.items()) + player_prog_items = self.prog_items[player] + for item, count in item_counts.items(): + if player_prog_items[item] >= count: + return True + return False def count(self, item: str, player: int) -> int: return self.prog_items[player][item] @@ -911,11 +930,20 @@ def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" - return sum(self.prog_items[player][item_name] for item_name in items) + player_prog_items = self.prog_items[player] + total = 0 + for item_name in items: + total += player_prog_items[item_name] + return total def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" - return sum(self.prog_items[player][item_name] > 0 for item_name in items) + player_prog_items = self.prog_items[player] + total = 0 + for item_name in items: + if player_prog_items[item_name] > 0: + total += 1 + return total # item name group related def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: From 628252896e41d5d3e10414149a7bf50eed1307e3 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 3 Feb 2025 09:53:56 -0500 Subject: [PATCH 40/60] TUNIC: Call Combat Logic experimental (#4594) * Update options.py * Update options.py --- worlds/tunic/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index d2ea82803704..8fe2ea5ce854 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -197,6 +197,7 @@ class TunicPlandoConnections(PlandoConnections): class CombatLogic(Choice): """ + EXPERIMENTAL - may cause gen failures, especially when playthrough generation for the spoiler log is enabled, and may have slight logic issues. If enabled, the player will logically require a combination of stat upgrade items and equipment to get some checks or navigate to some areas, with a goal of matching the vanilla combat difficulty. The player may still be expected to run past enemies, reset aggro (by using a checkpoint or doing a scene transition), or find sneaky paths to checks. This option marks many more items as progression and may force weapons much earlier than normal. From 19faaa4104a97cac4a7980454ba55dae42758ea7 Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 3 Feb 2025 19:49:07 -0500 Subject: [PATCH 41/60] Core: Fix #4595 by using first type's docstring in a union type (#4600) * Fix #4595: use first type's docstring in a union type. * Reuse existing import. --- settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings.py b/settings.py index cc808c2732df..14d4ba30cefb 100644 --- a/settings.py +++ b/settings.py @@ -282,7 +282,8 @@ def dump(self, f: TextIO, level: int = 0) -> None: 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) - while attr_cls_origin is Union: # resolve to first type for doc string + # resolve to first type for doc string + while attr_cls_origin is Union or attr_cls_origin is types.UnionType: attr_cls = typing.get_args(attr_cls)[0] attr_cls_origin = typing.get_origin(attr_cls) if attr_cls.__doc__ and attr_cls.__module__ != "builtins": From da48af60dc443526cf28a16568e6cdb9d5732f09 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:27:23 -0500 Subject: [PATCH 42/60] Stardew Valley: add assert_can_reach_region_* for better tests (#4556) * add assert_reach_region_*; refactor existing assert_reach_location_* to allow string * rename asserts --- worlds/stardew_valley/test/TestBooksanity.py | 8 ++-- .../stardew_valley/test/TestWalnutsanity.py | 4 +- .../test/assertion/rule_assert.py | 41 +++++++++++++++---- .../test/assertion/world_assert.py | 2 +- worlds/stardew_valley/test/rules/TestBooks.py | 8 ++-- .../stardew_valley/test/rules/TestFishing.py | 6 +-- .../stardew_valley/test/rules/TestSkills.py | 10 ++--- 7 files changed, 50 insertions(+), 29 deletions(-) diff --git a/worlds/stardew_valley/test/TestBooksanity.py b/worlds/stardew_valley/test/TestBooksanity.py index 942f35d961a9..3c737e502c64 100644 --- a/worlds/stardew_valley/test/TestBooksanity.py +++ b/worlds/stardew_valley/test/TestBooksanity.py @@ -65,7 +65,7 @@ def test_can_ship_all_books(self): if item_to_ship not in power_books and item_to_ship not in skill_books: continue with self.subTest(location.name): - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) class TestBooksanityPowers(SVTestBase): @@ -111,7 +111,7 @@ def test_can_ship_all_books(self): if item_to_ship not in power_books and item_to_ship not in skill_books: continue with self.subTest(location.name): - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) class TestBooksanityPowersAndSkills(SVTestBase): @@ -157,7 +157,7 @@ def test_can_ship_all_books(self): if item_to_ship not in power_books and item_to_ship not in skill_books: continue with self.subTest(location.name): - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) class TestBooksanityAll(SVTestBase): @@ -203,4 +203,4 @@ def test_can_ship_all_books(self): if item_to_ship not in power_books and item_to_ship not in skill_books: continue with self.subTest(location.name): - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py index da17d749eaed..862553dee1cb 100644 --- a/worlds/stardew_valley/test/TestWalnutsanity.py +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -81,10 +81,10 @@ def test_field_office_locations_require_professor_snail(self): self.collect("Combat Level", 10) self.collect("Mining Level", 10) for location in locations: - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.collect("Open Professor Snail Cave") for location in locations: - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) class TestWalnutsanityBushes(SVTestBase): diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index 1031a18e115c..02362f2d150d 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,7 +1,7 @@ from typing import List from unittest import TestCase -from BaseClasses import CollectionState, Location +from BaseClasses import CollectionState, Location, Region from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach from ...stardew_rule.rule_explain import explain @@ -40,19 +40,42 @@ def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: Collect raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") - def assert_reach_location_true(self, location: Location, state: CollectionState): - expl = explain(Reach(location.name, "Location", 1), state) + def assert_can_reach_location(self, location: Location | str, state: CollectionState) -> None: + location_name = location.name if isinstance(location, Location) else location + expl = explain(Reach(location_name, "Location", 1), state) try: - can_reach = location.can_reach(state) + can_reach = state.can_reach_location(location_name, 1) self.assertTrue(can_reach, expl) except KeyError as e: - raise AssertionError(f"Error while checking location {location.name}: {e}" + raise AssertionError(f"Error while checking location {location_name}: {e}" f"\nExplanation: {expl}") - def assert_reach_location_false(self, location: Location, state: CollectionState): - expl = explain(Reach(location.name, "Location", 1), state, expected=False) + def assert_cannot_reach_location(self, location: Location | str, state: CollectionState) -> None: + location_name = location.name if isinstance(location, Location) else location + expl = explain(Reach(location_name, "Location", 1), state, expected=False) try: - self.assertFalse(location.can_reach(state), expl) + can_reach = state.can_reach_location(location_name, 1) + self.assertFalse(can_reach, expl) except KeyError as e: - raise AssertionError(f"Error while checking location {location.name}: {e}" + raise AssertionError(f"Error while checking location {location_name}: {e}" + f"\nExplanation: {expl}") + + def assert_can_reach_region(self, region: Region | str, state: CollectionState) -> None: + region_name = region.name if isinstance(region, Region) else region + expl = explain(Reach(region_name, "Region", 1), state) + try: + can_reach = state.can_reach_region(region_name, 1) + self.assertTrue(can_reach, expl) + except KeyError as e: + raise AssertionError(f"Error while checking region {region_name}: {e}" + f"\nExplanation: {expl}") + + def assert_cannot_reach_region(self, region: Region | str, state: CollectionState) -> None: + region_name = region.name if isinstance(region, Region) else region + expl = explain(Reach(region_name, "Region", 1), state, expected=False) + try: + can_reach = state.can_reach_region(region_name, 1) + self.assertFalse(can_reach, expl) + except KeyError as e: + raise AssertionError(f"Error while checking region {region_name}: {e}" f"\nExplanation: {expl}") diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index 97172834543c..97f5376058cb 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -53,7 +53,7 @@ def assert_same_number_items_locations(self, multiworld: MultiWorld): def assert_can_reach_everything(self, multiworld: MultiWorld): for location in multiworld.get_locations(): - self.assert_reach_location_true(location, multiworld.state) + self.assert_can_reach_location(location, multiworld.state) def assert_basic_checks(self, multiworld: MultiWorld): self.assert_same_number_items_locations(multiworld) diff --git a/worlds/stardew_valley/test/rules/TestBooks.py b/worlds/stardew_valley/test/rules/TestBooks.py index 6605e7e645e3..af0055d2282d 100644 --- a/worlds/stardew_valley/test/rules/TestBooks.py +++ b/worlds/stardew_valley/test/rules/TestBooks.py @@ -12,15 +12,13 @@ def test_need_weapon_for_mapping_cave_systems(self): location = self.multiworld.get_location("Read Mapping Cave Systems", self.player) - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.collect("Progressive Mine Elevator") self.collect("Progressive Mine Elevator") self.collect("Progressive Mine Elevator") self.collect("Progressive Mine Elevator") - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.collect("Progressive Weapon") - self.assert_reach_location_true(location, self.multiworld.state) - - + self.assert_can_reach_location(location, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestFishing.py b/worlds/stardew_valley/test/rules/TestFishing.py index 513bb951e933..74a33f36686f 100644 --- a/worlds/stardew_valley/test/rules/TestFishing.py +++ b/worlds/stardew_valley/test/rules/TestFishing.py @@ -43,18 +43,18 @@ def test_catch_fish_requires_region_unlock(self): self.collect_all_the_money() item_names = fish_and_items[fish] location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player) - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) items = [] for item_name in item_names: items.append(self.collect(item_name)) with self.subTest(f"{fish} can be reached with {item_names}"): - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) for item_required in items: self.multiworld.state = self.original_state.copy() with self.subTest(f"{fish} requires {item_required.name}"): for item_to_collect in items: if item_to_collect.name != item_required.name: self.collect(item_to_collect) - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.multiworld.state = self.original_state.copy() diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py index 77adade886dc..ee605bfaa161 100644 --- a/worlds/stardew_valley/test/rules/TestSkills.py +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -39,10 +39,10 @@ def test_all_skill_levels_require_previous_level(self): with self.subTest(location_name): if level > 1: - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.collect(f"{skill} Level") - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) self.reset_collection_state() @@ -88,7 +88,7 @@ def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self): for skill in all_vanilla_skills: with self.subTest(skill): location = self.multiworld.get_location(f"{skill} Mastery", self.player) - self.assert_reach_location_true(location, self.multiworld.state) + self.assert_can_reach_location(location, self.multiworld.state) self.reset_collection_state() @@ -99,7 +99,7 @@ def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery( self.remove_one_by_name(f"{skill} Level") location = self.multiworld.get_location(f"{skill} Mastery", self.player) - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.reset_collection_state() @@ -108,6 +108,6 @@ def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(s self.remove_one_by_name(f"Progressive Pickaxe") location = self.multiworld.get_location("Mining Mastery", self.player) - self.assert_reach_location_false(location, self.multiworld.state) + self.assert_cannot_reach_location(location, self.multiworld.state) self.reset_collection_state() From db11c620a746b23c46216cae1e2f05a013aeb341 Mon Sep 17 00:00:00 2001 From: shananas <47014056+shananas@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:09:02 -0500 Subject: [PATCH 43/60] =?UTF-8?q?KH2=20Doc=20Update=C2=A0#4609?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mod Manager Version Number --- worlds/kh2/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index bee60bd36b18..2e1022f3efa7 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -10,7 +10,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/) - Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) - 1. Version 3.4.0 or greater OpenKH Mod Manager with Panacea + 1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea 2. Lua Backend from the OpenKH Mod Manager 3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager - Needed for Archipelago From f6668997e61a0e2ea53bf2e6f92a070686659f8c Mon Sep 17 00:00:00 2001 From: Martmists Date: Fri, 7 Feb 2025 21:02:37 +0100 Subject: [PATCH 44/60] [AHIT] Fix small options issue (#4615) --- worlds/ahit/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 17c4b95efc7a..b331ca524244 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -338,7 +338,7 @@ class MinExtraYarn(Range): There must be at least this much more yarn over the total number of yarn needed to craft all hats. For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, there must be at least 50 yarn in the pool.""" - display_name = "Max Extra Yarn" + display_name = "Min Extra Yarn" range_start = 5 range_end = 15 default = 10 From 768ccffe722551f6225c70003906f2b787bffdd0 Mon Sep 17 00:00:00 2001 From: Kory Dondzila Date: Fri, 7 Feb 2025 15:06:06 -0500 Subject: [PATCH 45/60] Shivers: Update shivers links and guides (#4592) --- worlds/shivers/docs/en_Shivers.md | 4 +++- worlds/shivers/docs/setup_en.md | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md index 9490b577bdd0..f36cbcce36e1 100644 --- a/worlds/shivers/docs/en_Shivers.md +++ b/worlds/shivers/docs/en_Shivers.md @@ -9,6 +9,7 @@ configuration file. All Ixupi pot pieces are randomized. Keys have been added to the game to lock off different rooms in the museum, these are randomized. Crawling has been added and is required to use any crawl space. +Randomization can also control if Ixupi pots are in pieces, mixed, or complete, and in which worlds they will show up in. ## What is considered a location check in Shivers? @@ -27,4 +28,5 @@ Victory is achieved when the player has captured the required number Ixupi set i ## Encountered a bug? -Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer. +Please contact GodlFire or Cynbel_Terreus on Discord for bugs related to Shivers world generation or the Shivers Randomizer. +You may also open issues for the Shivers Randomizer Client [here](https://github.com/Shivers-Randomizer/Shivers-Randomizer/issues). diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md index a495c87b226a..5d73a81b2967 100644 --- a/worlds/shivers/docs/setup_en.md +++ b/worlds/shivers/docs/setup_en.md @@ -5,12 +5,12 @@ - [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc - [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later -- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version +- [Shivers Randomizer Client](https://github.com/Shivers-Randomizer/Shivers-Randomizer/releases/latest) Latest release version ## Optional Software - [PopTracker](https://github.com/black-sliver/PopTracker/releases/) - - [Jax's Shivers PopTracker pack](https://github.com/blazik-barth/Shivers-Tracker/releases/) + - [Shivers PopTracker pack](https://github.com/Shivers-Randomizer/Shivers-AP-Tracker/releases/latest) ## Setup ScummVM for Shivers @@ -59,7 +59,9 @@ validator page: [YAML Validation page](/mysterycheck) ## What is a check -- Every puzzle -- Every puzzle hint/solution -- Every document that is considered a Flashback +- All puzzles +- All puzzle hints or solutions +- All documents that are considered Flashbacks +- All Ixupi captures (Lightning only if early) - Optionally information plaques +- Optionally elevators From f75a1ae1174fb467e5c5bd5568d7de3c806d5b1c Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 8 Feb 2025 00:06:04 +0100 Subject: [PATCH 46/60] KH2: Fix lambda capture issue with weapon slot logic (#4604) * KH2: Fix lambda capture issue with weapon slot logic * Update Rules.py * Improved by JaredWeakStrike (#4605) * Apparently this wasn't meant to be indented --------- Co-authored-by: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> --- worlds/kh2/Rules.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 767c5643417e..a59fbfd8ab97 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -263,7 +263,10 @@ def set_kh2_rules(self) -> None: weapon_region = self.multiworld.get_region(RegionName.Keyblade, self.player) for location in weapon_region.locations: - add_rule(location, lambda state: state.has(exclusion_table["WeaponSlots"][location.name], self.player)) + if location.name in exclusion_table["WeaponSlots"]: # shop items and starting items are not in this list + exclusion_item = exclusion_table["WeaponSlots"][location.name] + add_rule(location, lambda state, e_item=exclusion_item: state.has(e_item, self.player)) + if location.name in Goofy_Checks: add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys()) elif location.name in Donald_Checks: From f5c574c37ac6283cb360432e6c5b5cc35b2d1780 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 9 Feb 2025 06:11:27 -0500 Subject: [PATCH 47/60] Settings: add format handling to yaml exception marks for readability (#4531) --- settings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 14d4ba30cefb..255c537fe09a 100644 --- a/settings.py +++ b/settings.py @@ -792,7 +792,17 @@ def __init__(self, location: Optional[str]): # change to PathLike[str] once we if location: from Utils import parse_yaml with open(location, encoding="utf-8-sig") as f: - options = parse_yaml(f.read()) + from yaml.error import MarkedYAMLError + try: + options = parse_yaml(f.read()) + except MarkedYAMLError as ex: + if ex.problem_mark: + f.seek(0) + lines = f.readlines() + problem_line = lines[ex.problem_mark.line] + error_line = " " * ex.problem_mark.column + "^" + raise Exception(f"{ex.context} {ex.problem}\n{problem_line}{error_line}") + raise ex # TODO: detect if upgrade is required # TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing self.update(options or {}) From 359f45d50f3872fe097b6e605faf494398c8ff8a Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 9 Feb 2025 13:12:17 -0500 Subject: [PATCH 48/60] TUNIC: Combat logic fix (#4589) * Potential fix for attack issue * also put the lazy version of the swamp fix in for good measure * fix extra line * now it is good * Add the test, roll the other PR into this one * Make the test exception more useful * Remove debug print * Combat logic fixed? * Move a few areas to before well instead of east forest * Put in qwint's suggestions in test * Implement qwint's suggestions in combat_logic.py * Implement qwint's suggestions for combat_logic.py * Fix typo * Remove experimental from combat logic description * Remove copy_mixin again * Add comment about copy_mixin * Use a more proper random * Some optimizations from Vi's comments --- worlds/tunic/combat_logic.py | 308 +++++++++++++++++-------------- worlds/tunic/er_rules.py | 20 +- worlds/tunic/options.py | 1 - worlds/tunic/test/test_combat.py | 83 +++++++++ 4 files changed, 261 insertions(+), 151 deletions(-) create mode 100644 worlds/tunic/test/test_combat.py diff --git a/worlds/tunic/combat_logic.py b/worlds/tunic/combat_logic.py index 9ff363942c9e..2e490d1dad6e 100644 --- a/worlds/tunic/combat_logic.py +++ b/worlds/tunic/combat_logic.py @@ -8,6 +8,7 @@ # the vanilla stats you are expected to have to get through an area, based on where they are in vanilla class AreaStats(NamedTuple): + """Attack, Defense, Potion, HP, SP, MP, Flasks, Equipment, is_boss""" att_level: int def_level: int potion_level: int # all 3 are before your first bonfire after getting the upgrade page, third costs 1k @@ -41,7 +42,7 @@ class AreaStats(NamedTuple): "Rooted Ziggurat": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"]), "Boss Scavenger": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"], is_boss=True), "Swamp": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]), - "Cathedral": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]), + # Cathedral has the same requirements as Swamp # marked as boss because the garden knights can't get hurt by stick "Gauntlet": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"], is_boss=True), "The Heir": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic", "Laurels"], is_boss=True), @@ -49,8 +50,10 @@ class AreaStats(NamedTuple): # these are used for caching which areas can currently be reached in state +# Gauntlet does not have exclusively higher stat requirements, so it will be checked separately boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"] -non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss] +# Swamp does not have exclusively higher stat requirements, so it will be checked separately +non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"] class CombatState(IntEnum): @@ -89,6 +92,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool elif area_name in non_boss_areas: area_list = non_boss_areas else: + # this is to check Swamp and Gauntlet on their own area_list = [area_name] if met_combat_reqs: @@ -114,88 +118,99 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d extra_att_needed = 0 extra_def_needed = 0 extra_mp_needed = 0 - has_magic = state.has_any({"Magic Wand", "Gun"}, player) - stick_bool = False - sword_bool = False + has_magic = state.has_any(("Magic Wand", "Gun"), player) + sword_bool = has_sword(state, player) + stick_bool = sword_bool or has_melee(state, player) + equipment = data.equipment.copy() for item in data.equipment: if item == "Stick": - if not has_melee(state, player): + if not stick_bool: if has_magic: + equipment.remove("Stick") + if "Magic" not in equipment: + equipment.append("Magic") # magic can make up for the lack of stick extra_mp_needed += 2 - extra_att_needed -= 16 + extra_att_needed -= 32 else: return False - else: - stick_bool = True elif item == "Sword": - if not has_sword(state, player): + if not sword_bool: # need sword for bosses if data.is_boss: return False + equipment.remove("Sword") if has_magic: + if "Magic" not in equipment: + equipment.append("Magic") # +4 mp pretty much makes up for the lack of sword, at least in Quarry extra_mp_needed += 4 - # stick is a backup plan, and doesn't scale well, so let's require a little less - extra_att_needed -= 2 - elif has_melee(state, player): + if stick_bool: + # stick is a backup plan, and doesn't scale well, so let's require a little less + equipment.append("Stick") + extra_att_needed -= 2 + else: + extra_mp_needed += 2 + extra_att_needed -= 32 + elif stick_bool: + equipment.append("Stick") # may revise this later based on feedback extra_att_needed += 3 extra_def_needed += 2 else: return False - else: - sword_bool = True + # just increase the stat requirement, we'll check for shield when calculating defense elif item == "Shield": - if not state.has("Shield", player): - extra_def_needed += 2 + equipment.remove("Shield") + extra_def_needed += 2 + elif item == "Laurels": if not state.has("Hero's Laurels", player): - # these are entirely based on vibes - extra_att_needed += 2 - extra_def_needed += 3 + # require Laurels for the Heir + return False + elif item == "Magic": if not has_magic: + equipment.remove("Magic") extra_att_needed += 2 extra_def_needed += 2 - extra_mp_needed -= 16 + extra_mp_needed -= 32 + modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level, - data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count) - if not has_required_stats(modified_stats, state, player): + data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count, + equipment, data.is_boss) + if has_required_stats(modified_stats, state, player): + return True + else: # we may need to check if you would have the required stats if you were missing a weapon - # it's kinda janky, but these only get hit in less than once per 100 generations, so whatever - if sword_bool and "Sword" in data.equipment and "Magic" in data.equipment: - # we need to check if you would have the required stats if you didn't have melee - equip_list = [item for item in data.equipment if item != "Sword"] - more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level, - data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count, - equip_list) + if sword_bool and "Sword" in equipment and has_magic: + # we need to check if you would have the required stats if you didn't have the sword + equip_list = [item for item in equipment if item != "Sword"] + if "Magic" not in equip_list: + equip_list.append("Magic") + more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level, + modified_stats.potion_level, modified_stats.hp_level, + modified_stats.sp_level, modified_stats.mp_level + 4, + modified_stats.potion_count, equip_list, data.is_boss) if check_combat_reqs("none", state, player, more_modified_stats): return True - # and we need to check if you would have the required stats if you didn't have magic - equip_list = [item for item in data.equipment if item != "Magic"] - more_modified_stats = AreaStats(data.att_level + 2, data.def_level + 2, data.potion_level, - data.hp_level, data.sp_level, data.mp_level - 16, data.potion_count, - equip_list) - if check_combat_reqs("none", state, player, more_modified_stats): - return True - return False - - elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment: + elif stick_bool and "Stick" in equipment and has_magic: # we need to check if you would have the required stats if you didn't have the stick - equip_list = [item for item in data.equipment if item != "Stick"] - more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level, - data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count, - equip_list) + equip_list = [item for item in equipment if item != "Stick"] + if "Magic" not in equip_list: + equip_list.append("Magic") + more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level, + modified_stats.potion_level, modified_stats.hp_level, + modified_stats.sp_level, modified_stats.mp_level + 4, + modified_stats.potion_count, equip_list, data.is_boss) if check_combat_reqs("none", state, player, more_modified_stats): return True - return False else: return False - return True + return False # check if you have the required stats, and the money to afford them @@ -203,72 +218,63 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d # but that's fine -- it's already pretty generous to begin with def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> bool: money_required = 0 - player_att = 0 + att_required = data.att_level + player_att, att_offerings = get_att_level(state, player) - # check if we actually need the stat before checking state - if data.att_level > 1: - player_att, att_offerings = get_att_level(state, player) - if player_att < data.att_level: - return False + # if you have 2 more attack than needed, we can forego needing mp + if data.mp_level > 1: + if player_att < data.att_level + 2: + player_mp, mp_offerings = get_mp_level(state, player) + if player_mp < data.mp_level: + return False + else: + extra_mp = player_mp - data.mp_level + paid_mp = max(0, mp_offerings - extra_mp) + # mp costs 300 for the first, +50 for each additional + money_per_mp = 300 + for _ in range(paid_mp): + money_required += money_per_mp + money_per_mp += 50 else: - extra_att = player_att - data.att_level - paid_att = max(0, att_offerings - extra_att) - # attack upgrades cost 100 for the first, +50 for each additional - money_per_att = 100 - for _ in range(paid_att): - money_required += money_per_att - money_per_att += 50 + att_required += 2 + + if player_att < att_required: + return False + else: + extra_att = player_att - att_required + paid_att = max(0, att_offerings - extra_att) + # attack upgrades cost 100 for the first, +50 for each additional + money_per_att = 100 + for _ in range(paid_att): + money_required += money_per_att + money_per_att += 50 # adding defense and sp together since they accomplish similar things: making you take less damage if data.def_level + data.sp_level > 2: player_def, def_offerings = get_def_level(state, player) player_sp, sp_offerings = get_sp_level(state, player) - if player_def + player_sp < data.def_level + data.sp_level: + req_stats = data.def_level + data.sp_level + if player_def + player_sp < req_stats: return False else: free_def = player_def - def_offerings free_sp = player_sp - sp_offerings - paid_stats = data.def_level + data.sp_level - free_def - free_sp - sp_to_buy = 0 - - if paid_stats <= 0: - # if you don't have to pay for any stats, you don't need money for these upgrades - def_to_buy = 0 - elif paid_stats <= def_offerings: - # get the amount needed to buy these def offerings - def_to_buy = paid_stats + if free_sp + free_def >= req_stats: + # you don't need to buy upgrades + pass else: - def_to_buy = def_offerings - sp_to_buy = max(0, paid_stats - def_offerings) - - # if you have to buy more than 3 def, it's cheaper to buy 1 extra sp - if def_to_buy > 3 and sp_offerings > 0: - def_to_buy -= 1 - sp_to_buy += 1 - # def costs 100 for the first, +50 for each additional - money_per_def = 100 - for _ in range(def_to_buy): - money_required += money_per_def - money_per_def += 50 - # sp costs 200 for the first, +200 for each additional - money_per_sp = 200 - for _ in range(sp_to_buy): - money_required += money_per_sp - money_per_sp += 200 - - # if you have 2 more attack than needed, we can forego needing mp - if data.mp_level > 1 and player_att < data.att_level + 2: - player_mp, mp_offerings = get_mp_level(state, player) - if player_mp < data.mp_level: - return False - else: - extra_mp = player_mp - data.mp_level - paid_mp = max(0, mp_offerings - extra_mp) - # mp costs 300 for the first, +50 for each additional - money_per_mp = 300 - for _ in range(paid_mp): - money_required += money_per_mp - money_per_mp += 50 + # we need to pick the cheapest option that gets us above the stats we need + # first number is def, second number is sp + upgrade_options: set[tuple[int, int]] = set() + stats_to_buy = req_stats - free_def - free_sp + for paid_def in range(0, min(def_offerings + 1, stats_to_buy + 1)): + sp_required = stats_to_buy - paid_def + if sp_offerings >= sp_required: + if sp_required < 0: + break + upgrade_options.add((paid_def, stats_to_buy - paid_def)) + costs = [calc_def_sp_cost(defense, sp) for defense, sp in upgrade_options] + money_required += min(costs) req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count) player_potion, potion_offerings = get_potion_level(state, player) @@ -279,53 +285,30 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> return False else: # need a way to determine which of potion offerings or hp offerings you can reduce - # your level if you didn't pay for offerings free_potion = player_potion - potion_offerings free_hp = player_hp - hp_offerings - paid_hp_count = 0 - paid_potion_count = 0 if calc_effective_hp(free_hp, free_potion, player_potion_count) >= req_effective_hp: # you don't need to buy upgrades pass - # if you have no potions, or no potion upgrades, you only need to check your hp upgrades - elif player_potion_count == 0 or potion_offerings == 0: - # check if you have enough hp at each paid hp offering - for i in range(hp_offerings): - paid_hp_count = i + 1 - if calc_effective_hp(paid_hp_count, 0, player_potion_count) > req_effective_hp: - break else: - for i in range(potion_offerings): - paid_potion_count = i + 1 - if calc_effective_hp(free_hp, free_potion + paid_potion_count, player_potion_count) > req_effective_hp: - break - for j in range(hp_offerings): - paid_hp_count = j + 1 - if (calc_effective_hp(free_hp + paid_hp_count, free_potion + paid_potion_count, player_potion_count) - > req_effective_hp): + # we need to pick the cheapest option that gets us above the amount of effective HP we need + # first number is hp, second number is potion + upgrade_options: set[tuple[int, int]] = set() + # filter out exclusively worse options + lowest_hp_added = hp_offerings + 1 + for paid_potion in range(0, potion_offerings + 1): + # check quantities of hp offerings for each potion offering + for paid_hp in range(0, lowest_hp_added): + if (calc_effective_hp(free_hp + paid_hp, free_potion + paid_potion, player_potion_count) + >= req_effective_hp): + upgrade_options.add((paid_hp, paid_potion)) + lowest_hp_added = paid_hp break - # hp costs 200 for the first, +50 for each additional - money_per_hp = 200 - for _ in range(paid_hp_count): - money_required += money_per_hp - money_per_hp += 50 - - # potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional - # currently we assume you will not buy past the second potion upgrade, but we might change our minds later - money_per_potion = 100 - for _ in range(paid_potion_count): - money_required += money_per_potion - if money_per_potion == 100: - money_per_potion = 300 - elif money_per_potion == 300: - money_per_potion = 1000 - else: - money_per_potion += 200 - if money_required > get_money_count(state, player): - return False + costs = [calc_hp_potion_cost(hp, potion) for hp, potion in upgrade_options] + money_required += min(costs) - return True + return get_money_count(state, player) >= money_required # returns a tuple of your max attack level, the number of attack offerings @@ -336,7 +319,8 @@ def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]: if sword_level >= 3: att_upgrades += min(2, sword_level - 2) # attack falls off, can just cap it at 8 for simplicity - return min(8, 1 + att_offerings + att_upgrades), att_offerings + return (min(8, 1 + att_offerings + att_upgrades) + + (1 if state.has("Hero's Laurels", player) else 0), att_offerings) # returns a tuple of your max defense level, the number of defense offerings @@ -344,7 +328,9 @@ def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]: def_offerings = state.count("DEF Offering", player) # defense falls off, can just cap it at 8 for simplicity return (min(8, 1 + def_offerings - + state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)), + + state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)) + + (2 if state.has("Shield", player) else 0) + + (2 if state.has("Hero's Laurels", player) else 0), def_offerings) @@ -408,6 +394,46 @@ def get_money_count(state: CollectionState, player: int) -> int: return money +def calc_hp_potion_cost(hp_upgrades: int, potion_upgrades: int) -> int: + money = 0 + + # hp costs 200 for the first, +50 for each additional + money_per_hp = 200 + for _ in range(hp_upgrades): + money += money_per_hp + money_per_hp += 50 + + # potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional + # currently we assume you will not buy past the second potion upgrade, but we might change our minds later + money_per_potion = 100 + for _ in range(potion_upgrades): + money += money_per_potion + if money_per_potion == 100: + money_per_potion = 300 + elif money_per_potion == 300: + money_per_potion = 1000 + else: + money_per_potion += 200 + + return money + + +def calc_def_sp_cost(def_upgrades: int, sp_upgrades: int) -> int: + money = 0 + + money_per_def = 100 + for _ in range(def_upgrades): + money += money_per_def + money_per_def += 50 + + money_per_sp = 200 + for _ in range(sp_upgrades): + money += money_per_sp + money_per_sp += 200 + + return money + + class TunicState(LogicMixin): tunic_need_to_reset_combat_from_collect: Dict[int, bool] tunic_need_to_reset_combat_from_remove: Dict[int, bool] @@ -420,3 +446,5 @@ def init_mixin(self, _): self.tunic_need_to_reset_combat_from_remove = defaultdict(lambda: False) # the per-player, per-area state of combat checking -- unchecked, failed, or succeeded self.tunic_area_combat_state = defaultdict(lambda: defaultdict(lambda: CombatState.unchecked)) + # a copy_mixin was intentionally excluded because the empty state from init_mixin + # will always be appropriate for recalculating the logic cache diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 08b088f7e4a7..4d0a462cbb8a 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1386,9 +1386,9 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: # need to fight through the rudelings and turret, or just laurels from near the windmill set_rule(ow_to_well_entry, lambda state: state.has(laurels, player) - or has_combat_reqs("East Forest", state, player)) + or has_combat_reqs("Before Well", state, player)) set_rule(ow_tunnel_beach, - lambda state: has_combat_reqs("East Forest", state, player)) + lambda state: has_combat_reqs("Before Well", state, player)) add_rule(atoll_statue, lambda state: has_combat_reqs("Ruined Atoll", state, player)) @@ -1467,12 +1467,12 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: set_rule(cath_entry_to_elev, lambda state: options.entrance_rando or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - or (has_ability(prayer, state, world) and has_combat_reqs("Cathedral", state, player))) + or (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player))) set_rule(cath_entry_to_main, - lambda state: has_combat_reqs("Cathedral", state, player)) + lambda state: has_combat_reqs("Swamp", state, player)) set_rule(cath_elev_to_main, - lambda state: has_combat_reqs("Cathedral", state, player)) + lambda state: has_combat_reqs("Swamp", state, player)) # for spots where you can go into and come out of an entrance to reset enemy aggro if world.options.entrance_rando: @@ -1835,10 +1835,10 @@ def combat_logic_to_loc(loc_name: str, combat_req_area: str, set_instead: bool = combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True) combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True) combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld") - combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "East Forest", dagger=True) - combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "East Forest", dagger=True) - combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "East Forest", dagger=True) - combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "East Forest", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "Before Well", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "Before Well", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "Before Well", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "Before Well", dagger=True) combat_logic_to_loc("Overworld - [Northwest] Chest Near Turret", "Before Well") add_rule(world.get_location("Hourglass Cave - Hourglass Chest"), @@ -1927,4 +1927,4 @@ def combat_logic_to_loc(loc_name: str, combat_req_area: str, set_instead: bool = # zip through the rubble to sneakily grab this chest, or just fight to it add_rule(world.get_location("Cathedral - [1F] Near Spikes"), - lambda state: laurels_zip(state, world) or has_combat_reqs("Cathedral", state, player)) + lambda state: laurels_zip(state, world) or has_combat_reqs("Swamp", state, player)) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 8fe2ea5ce854..d2ea82803704 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -197,7 +197,6 @@ class TunicPlandoConnections(PlandoConnections): class CombatLogic(Choice): """ - EXPERIMENTAL - may cause gen failures, especially when playthrough generation for the spoiler log is enabled, and may have slight logic issues. If enabled, the player will logically require a combination of stat upgrade items and equipment to get some checks or navigate to some areas, with a goal of matching the vanilla combat difficulty. The player may still be expected to run past enemies, reset aggro (by using a checkpoint or doing a scene transition), or find sneaky paths to checks. This option marks many more items as progression and may force weapons much earlier than normal. diff --git a/worlds/tunic/test/test_combat.py b/worlds/tunic/test/test_combat.py new file mode 100644 index 000000000000..866dc5f81429 --- /dev/null +++ b/worlds/tunic/test/test_combat.py @@ -0,0 +1,83 @@ +from BaseClasses import ItemClassification +from collections import Counter + +from . import TunicTestBase +from .. import options +from ..combat_logic import (check_combat_reqs, area_data, get_money_count, calc_effective_hp, get_potion_level, + get_hp_level, get_def_level, get_sp_level) +from ..items import item_table +from .. import TunicWorld + + +class TestCombat(TunicTestBase): + options = {options.CombatLogic.internal_name: options.CombatLogic.option_on} + player = 1 + world: TunicWorld + combat_items = [] + # these are items that are progression that do not contribute to combat logic + # it's listed as using skipped items instead of a list of viable items so that if we add/remove some later, + # that this won't require updates most likely + # Stick and Sword are in here because sword progression is the clear determining case here + skipped_items = {"Fairy", "Stick", "Sword", "Magic Dagger", "Magic Orb", "Lantern", "Old House Key", "Key", + "Fortress Vault Key", "Golden Coin", "Red Questagon", "Green Questagon", "Blue Questagon", + "Scavenger Mask", "Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"} + # converts golden trophies to their hero relic stat equivalent, for easier parsing + converter = { + "Secret Legend": "Hero Relic - DEF", + "Phonomath": "Hero Relic - DEF", + "Just Some Pals": "Hero Relic - POTION", + "Spring Falls": "Hero Relic - POTION", + "Back To Work": "Hero Relic - POTION", + "Mr Mayor": "Hero Relic - SP", + "Power Up": "Hero Relic - SP", + "Regal Weasel": "Hero Relic - SP", + "Forever Friend": "Hero Relic - SP", + "Sacred Geometry": "Hero Relic - MP", + "Vintage": "Hero Relic - MP", + "Dusty": "Hero Relic - MP", + } + skipped_items.update({item for item in item_table.keys() if item.startswith("Ladder")}) + for item, data in item_table.items(): + if item in skipped_items: + continue + ic = data.combat_ic or data.classification + if item in converter: + item = converter[item] + if ItemClassification.progression in ic: + combat_items += [item] * data.quantity_in_item_pool + + # we had an issue where collecting certain items brought certain areas out of logic + # due to the weirdness of swapping between "you have enough attack that you don't need magic" + # so this will make sure collecting an item doesn't bring something out of logic + def test_combat_doesnt_fail_backwards(self): + combat_items = self.combat_items.copy() + self.multiworld.worlds[1].random.shuffle(combat_items) + curr_statuses = {name: False for name in area_data.keys()} + prev_statuses = curr_statuses.copy() + area_names = list(area_data.keys()) + current_items = Counter() + for current_item_name in combat_items: + current_items[current_item_name] += 1 + current_item = TunicWorld.create_item(self.world, current_item_name) + self.collect(current_item) + self.multiworld.worlds[1].random.shuffle(area_names) + for area in area_names: + curr_statuses[area] = check_combat_reqs(area, self.multiworld.state, self.player) + if curr_statuses[area] < prev_statuses[area]: + data = area_data[area] + state = self.multiworld.state + player = self.player + req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count) + player_potion, potion_offerings = get_potion_level(state, player) + player_hp, hp_offerings = get_hp_level(state, player) + player_def, def_offerings = get_def_level(state, player) + player_sp, sp_offerings = get_sp_level(state, player) + raise Exception(f"Status for {area} decreased after collecting {current_item_name}.\n" + f"Current items: {current_items}.\n" + f"Total money: {get_money_count(self.multiworld.state, self.player)}.\n" + f"Required Effective HP: {req_effective_hp}.\n" + f"Free HP and Offerings: {player_hp - hp_offerings}, {hp_offerings}\n" + f"Free Potion and Offerings: {player_potion - potion_offerings}, {potion_offerings}\n" + f"Free Def and Offerings: {player_def - def_offerings}, {def_offerings}\n" + f"Free SP and Offerings: {player_sp - sp_offerings}, {sp_offerings}") + prev_statuses[area] = curr_statuses[area] From 18bcaa85a27890de47e623c630a65dddb3d644d4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 10 Feb 2025 19:18:14 +0100 Subject: [PATCH 49/60] Test: ensure get_all_state() does not error in between steps (#4612) --- test/general/test_state.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/general/test_state.py diff --git a/test/general/test_state.py b/test/general/test_state.py new file mode 100644 index 000000000000..460fc3d60846 --- /dev/null +++ b/test/general/test_state.py @@ -0,0 +1,29 @@ +import unittest + +from worlds.AutoWorld import AutoWorldRegister, call_all +from . import setup_solo_multiworld + + +class TestBase(unittest.TestCase): + gen_steps = ( + "generate_early", + "create_regions", + ) + + test_steps = ( + "create_items", + "set_rules", + "connect_entrances", + "generate_basic", + "pre_fill", + ) + + def test_all_state_is_available(self): + """Ensure all_state can be created at certain steps.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game=game_name): + multiworld = setup_solo_multiworld(world_type, self.gen_steps) + for step in self.test_steps: + with self.subTest("Step", step=step): + call_all(multiworld, step) + self.assertTrue(multiworld.get_all_state(False, True)) From a298be9c41a60da209bf1eb6da15ff16f33350f1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 10 Feb 2025 19:19:00 +0100 Subject: [PATCH 50/60] Core: change HINT_FOUND to 40 and HINT_UNSPECIFIED to 0 (#4620) --- NetUtils.py | 4 ++-- docs/network protocol.md | 4 ++-- kvui.py | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/NetUtils.py b/NetUtils.py index 5bcc583c53b6..f2ae2a63a056 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -12,11 +12,11 @@ class HintStatus(ByValue, enum.IntEnum): - HINT_FOUND = 0 - HINT_UNSPECIFIED = 1 + HINT_UNSPECIFIED = 0 HINT_NO_PRIORITY = 10 HINT_AVOID = 20 HINT_PRIORITY = 30 + HINT_FOUND = 40 class JSONMessagePart(typing.TypedDict, total=False): diff --git a/docs/network protocol.md b/docs/network protocol.md index e5d3b7e6c26a..05a53344267e 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -363,11 +363,11 @@ An enumeration containing the possible hint states. ```python import enum class HintStatus(enum.IntEnum): - HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found. - HINT_UNSPECIFIED = 1 # The receiving player has not specified any status + HINT_UNSPECIFIED = 0 # The receiving player has not specified any status HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded HINT_AVOID = 20 # The receiving player has specified that the item is detrimental HINT_PRIORITY = 30 # The receiving player has specified that the item is needed + HINT_FOUND = 40 # The location has been collected. Status cannot be changed once found. ``` - Hints for items with `ItemClassification.trap` default to `HINT_AVOID`. - Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`. diff --git a/kvui.py b/kvui.py index 6718e48bee33..60042b00ec5c 100644 --- a/kvui.py +++ b/kvui.py @@ -444,8 +444,11 @@ def on_touch_down(self, touch): if child.collide_point(*touch.pos): key = child.sort_key if key == "status": - parent.hint_sorter = lambda element: element["status"]["hint"]["status"] - else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower() + parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]] + else: + parent.hint_sorter = lambda element: ( + remove_between_brackets.sub("", element[key]["text"]).lower() + ) if key == parent.sort_key: # second click reverses order parent.reversed = not parent.reversed @@ -829,7 +832,13 @@ def __init__(self, *args, **kwargs): HintStatus.HINT_AVOID: "salmon", HintStatus.HINT_PRIORITY: "plum", } - +status_sort_weights: dict[HintStatus, int] = { + HintStatus.HINT_FOUND: 0, + HintStatus.HINT_UNSPECIFIED: 1, + HintStatus.HINT_NO_PRIORITY: 2, + HintStatus.HINT_AVOID: 3, + HintStatus.HINT_PRIORITY: 4, +} class HintLog(RecycleView): From f4e43ca9e097f8301ce71bb0f53cbd9fca504a5d Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 10 Feb 2025 13:22:06 -0500 Subject: [PATCH 51/60] LttP: mock world.random in adjuster (#4623) --- LttPAdjuster.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 7e33a3d5efe8..963557e8da81 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -33,10 +33,15 @@ WINDOW_MIN_WIDTH = 425 class AdjusterWorld(object): + class AdjusterSubWorld(object): + def __init__(self, random): + self.random = random + def __init__(self, sprite_pool): import random self.sprite_pool = {1: sprite_pool} self.per_slot_randoms = {1: random} + self.worlds = {1: self.AdjusterSubWorld(random)} class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): From e9c463c897449202c4e958fe5a13947eee3325f2 Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 10 Feb 2025 13:23:09 -0500 Subject: [PATCH 52/60] CC: Force Text Client to always connect with empty game (#4607) --- CommonClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index 996ba3300575..eb38195216b6 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1095,7 +1095,7 @@ async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(TextContext, self).server_auth(password_requested) await self.get_username() - await self.send_connect() + await self.send_connect(game="") def on_package(self, cmd: str, args: dict): if cmd == "Connected": From dbf6b6f935c7b049c015a8a6830e078c769326ec Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 10 Feb 2025 13:23:58 -0500 Subject: [PATCH 53/60] CC: don't try to reconnect on invalid version (#4606) --- CommonClient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/CommonClient.py b/CommonClient.py index eb38195216b6..33792f0ed28b 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -907,6 +907,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.disconnected_intentionally = True ctx.event_invalid_game() elif 'IncompatibleVersion' in errors: + ctx.disconnected_intentionally = True raise Exception('Server reported your client version as incompatible. ' 'This probably means you have to update.') elif 'InvalidItemsHandling' in errors: From 910369a7f8d1e08744c616b59beedbeb5b2c3f90 Mon Sep 17 00:00:00 2001 From: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:27:10 -0600 Subject: [PATCH 54/60] Bizhawk Client: Display Err (#4532) Co-authored-by: Bryce Wilson --- worlds/_bizhawk/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index cb59050b84f6..accb5f94c482 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -238,6 +238,7 @@ def _patch_and_run_game(patch_file: str): return metadata except Exception as exc: logger.exception(exc) + Utils.messagebox("Error Patching Game", str(exc), True) return {} From f520c1d9f28d50850f7cb3b2cd58e5ad8984e43d Mon Sep 17 00:00:00 2001 From: qwint Date: Mon, 10 Feb 2025 13:34:27 -0500 Subject: [PATCH 55/60] Launcher: Allow for --nogui client launches (#4549) --- worlds/_bizhawk/client.py | 4 ++-- worlds/ahit/__init__.py | 4 ++-- worlds/factorio/__init__.py | 4 ++-- worlds/kh1/__init__.py | 4 ++-- worlds/kh2/__init__.py | 4 ++-- worlds/zork_grand_inquisitor/__init__.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index ce75b864b88c..16a8325a10f7 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -7,7 +7,7 @@ import abc from typing import TYPE_CHECKING, Any, ClassVar -from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess +from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch as launch_component if TYPE_CHECKING: from .context import BizHawkClientContext @@ -15,7 +15,7 @@ def launch_client(*args) -> None: from .context import launch - launch_subprocess(launch, name="BizHawkClient", args=args) + launch_component(launch, name="BizHawkClient", args=args) component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 14cf13ec346d..c2fe39872f31 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -12,13 +12,13 @@ from worlds.AutoWorld import World, WebWorld, CollectionState from worlds.generic.Rules import add_rule from typing import List, Dict, TextIO -from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type +from worlds.LauncherComponents import Component, components, icon_paths, launch as launch_component, Type from Utils import local_path def launch_client(): from .Client import launch - launch_subprocess(launch, name="AHITClient") + launch_component(launch, name="AHITClient") components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client, diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index ca9f12f1b21a..3f480527f549 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -8,7 +8,7 @@ import settings from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld -from worlds.LauncherComponents import Component, components, Type, launch_subprocess +from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.generic import Rules from .Locations import location_pools, location_table from .Mod import generate_mod @@ -24,7 +24,7 @@ def launch_client(): from .Client import launch - launch_subprocess(launch, name="FactorioClient") + launch_component(launch, name="FactorioClient") components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT)) diff --git a/worlds/kh1/__init__.py b/worlds/kh1/__init__.py index 3b498acf4670..ac0afca50142 100644 --- a/worlds/kh1/__init__.py +++ b/worlds/kh1/__init__.py @@ -9,12 +9,12 @@ 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 +from worlds.LauncherComponents import Component, components, Type, launch as launch_component def launch_client(): from .Client import launch - launch_subprocess(launch, name="KH1 Client") + launch_component(launch, name="KH1 Client") components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT)) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 59c77627eebe..edc4305accaf 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -3,7 +3,7 @@ from BaseClasses import Tutorial, ItemClassification from Fill import fast_fill -from worlds.LauncherComponents import Component, components, Type, launch_subprocess +from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.AutoWorld import World, WebWorld from .Items import * from .Locations import * @@ -17,7 +17,7 @@ def launch_client(): from .Client import launch - launch_subprocess(launch, name="KH2Client") + launch_component(launch, name="KH2Client") components.append(Component("KH2 Client", "KH2Client", func=launch_client, component_type=Type.CLIENT)) diff --git a/worlds/zork_grand_inquisitor/__init__.py b/worlds/zork_grand_inquisitor/__init__.py index 4da257e47bd0..791f41dd00a2 100644 --- a/worlds/zork_grand_inquisitor/__init__.py +++ b/worlds/zork_grand_inquisitor/__init__.py @@ -5,7 +5,7 @@ def launch_client() -> None: from .client import main - LauncherComponents.launch_subprocess(main, name="ZorkGrandInquisitorClient") + LauncherComponents.launch(main, name="ZorkGrandInquisitorClient") LauncherComponents.components.append( From f1769a8d0070dad489e35edb481e81ba0330c95f Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 12 Feb 2025 19:45:03 +0300 Subject: [PATCH 56/60] Stardew Valley: Fixed Powdermelon and option inconsistencies (#4632) * - Fixed powdermelon season * - Improve cohesion in presets * - Update several tooltips to be more consistent and accurate --- worlds/stardew_valley/content/vanilla/base.py | 2 +- worlds/stardew_valley/options/options.py | 35 ++++++++++--------- worlds/stardew_valley/options/presets.py | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/worlds/stardew_valley/content/vanilla/base.py b/worlds/stardew_valley/content/vanilla/base.py index 2c910df5d00f..9e5f53eb866e 100644 --- a/worlds/stardew_valley/content/vanilla/base.py +++ b/worlds/stardew_valley/content/vanilla/base.py @@ -140,7 +140,7 @@ def finalize_hook(self, content: StardewContent): Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),), Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),), - Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),), + Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.winter,)),), Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),), Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),), diff --git a/worlds/stardew_valley/options/options.py b/worlds/stardew_valley/options/options.py index f66ec3bdad80..aaeeedd1b3d8 100644 --- a/worlds/stardew_valley/options/options.py +++ b/worlds/stardew_valley/options/options.py @@ -66,7 +66,8 @@ def get_option_name(cls, value) -> str: class FarmType(Choice): - """What farm to play on?""" + """What farm to play on? + Custom farms are not supported""" internal_name = "farm_type" display_name = "Farm Type" default = "random" @@ -203,7 +204,7 @@ class SeasonRandomization(Choice): class Cropsanity(Choice): - """Formerly named "Seed Shuffle" + """ Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs. Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops Enabled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop @@ -233,9 +234,9 @@ class BackpackProgression(Choice): class ToolProgression(Choice): """Shuffle the tool upgrades? Vanilla: Clint will upgrade your tools with metal bars. - Progressive: You will randomly find Progressive Tool upgrades. - Cheap: Tool Upgrades will cost 2/5th as much - Very Cheap: Tool Upgrades will cost 1/5th as much""" + Progressive: Your tools upgrades are randomized. + Cheap: Tool Upgrades have a 60% discount + Very Cheap: Tool Upgrades have an 80% discount""" internal_name = "tool_progression" display_name = "Tool Progression" default = 1 @@ -279,8 +280,8 @@ class BuildingProgression(Choice): Vanilla: You can buy each building normally. Progressive: You will receive the buildings and will be able to build the first one of each type for free, once it is received. If you want more of the same building, it will cost the vanilla price. - Cheap: Buildings will cost half as much - Very Cheap: Buildings will cost 1/5th as much + Cheap: Buildings will have a 50% discount + Very Cheap: Buildings will an 80% discount """ internal_name = "building_progression" display_name = "Building Progression" @@ -327,7 +328,7 @@ class ArcadeMachineLocations(Choice): class SpecialOrderLocations(Choice): """Shuffle Special Orders? - Disabled: The special orders are not included in the Archipelago shuffling. + Vanilla: The special orders are not included in the Archipelago shuffling. You may need to complete some of them anyway for their vanilla rewards Board Only: The Special Orders on the board in town are location checks Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town Short: All Special Order requirements are reduced by 40% @@ -377,12 +378,12 @@ class QuestLocations(NamedRange): class Fishsanity(Choice): - """Locations for catching a fish the first time? + """Locations for catching each fish the first time? None: There are no locations for catching fish Legendaries: Each of the 5 legendary fish are checks, plus the extended family if qi board is turned on Special: A curated selection of strong fish are checks Randomized: A random selection of fish are checks - All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal + All: Every single fish in the game is a location that contains an item. Exclude Legendaries: Every fish except legendaries Exclude Hard Fish: Every fish under difficulty 80 Only Easy Fish: Every fish under difficulty 50 @@ -517,7 +518,7 @@ class Chefsanity(NamedRange): class Craftsanity(Choice): """Checks for crafting items? If enabled, all recipes purchased in shops will be checks as well. - Recipes obtained from other sources will depend on related archipelago settings + Recipes obtained from other sources will depend on their respective archipelago settings """ internal_name = "craftsanity" display_name = "Craftsanity" @@ -530,9 +531,9 @@ class Friendsanity(Choice): """Shuffle Friendships? None: Friendship hearts are earned normally Bachelors: Hearts with bachelors are shuffled - Starting NPCs: Hearts for NPCs available immediately are checks - All: Hearts for all npcs are checks, including Leo, Kent, Sandy, etc - All With Marriage: Hearts for all npcs are checks, including romance hearts up to 14 when applicable + Starting NPCs: Hearts for NPCs available immediately are shuffled + All: Hearts for all npcs are shuffled, including Leo, Kent, Sandy, etc + All With Marriage: All hearts for all npcs are shuffled, including romance hearts up to 14 when applicable """ internal_name = "friendsanity" display_name = "Friendsanity" @@ -577,7 +578,7 @@ class Walnutsanity(OptionSet): """Shuffle walnuts? Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame Bushes: Walnuts that are in a bush and can be collected by clicking it - Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts + Dig Spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc) """ internal_name = "walnutsanity" @@ -612,7 +613,7 @@ class NumberOfMovementBuffs(Range): class EnabledFillerBuffs(OptionSet): """Enable various permanent player buffs to roll as filler items - Luck: Increase daily luck + Luck: Increased daily luck Damage: Increased Damage % Defense: Increased Defense Immunity: Increased Immunity @@ -637,7 +638,7 @@ class EnabledFillerBuffs(OptionSet): class ExcludeGingerIsland(Toggle): """Exclude Ginger Island? This option will forcefully exclude everything related to Ginger Island from the slot. - If you pick a goal that requires Ginger Island, you cannot exclude it and it will get included anyway""" + If you pick a goal that requires Ginger Island, this option will get forced to 'false'""" internal_name = "exclude_ginger_island" display_name = "Exclude Ginger Island" default = 0 diff --git a/worlds/stardew_valley/options/presets.py b/worlds/stardew_valley/options/presets.py index c2c210e5ca6e..3dbb5ab3f554 100644 --- a/worlds/stardew_valley/options/presets.py +++ b/worlds/stardew_valley/options/presets.py @@ -122,7 +122,7 @@ options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs, options.FriendsanityHeartSize.internal_name: 4, options.Booksanity.internal_name: options.Booksanity.option_power_skill, - options.Walnutsanity.internal_name: [WalnutsanityOptionName.puzzles], + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, options.NumberOfMovementBuffs.internal_name: 6, options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, From b2162bb8e698fcc910377c3f76d5893c5f36ff3e Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 12 Feb 2025 11:46:07 -0500 Subject: [PATCH 57/60] Docs: clean up create_item/event example (#4596) * eyes * remove line wraps where unnecessary --- docs/world api.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index da74be70fb91..6a45ccbf99dc 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -562,17 +562,13 @@ from .items import is_progression # this is just a dummy def create_item(self, item: str) -> MyGameItem: # this is called when AP wants to create an item by name (for plando) or when you call it from your own code - classification = ItemClassification.progression if is_progression(item) else - ItemClassification.filler - - -return MyGameItem(item, classification, self.item_name_to_id[item], - self.player) + classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler + return MyGameItem(item, classification, self.item_name_to_id[item], self.player) def create_event(self, event: str) -> MyGameItem: # while we are at it, we can also add a helper to create events - return MyGameItem(event, True, None, self.player) + return MyGameItem(event, ItemClassification.progression, None, self.player) ``` #### create_items From 5c1ded1fe97a8f9fc5c69ac24d2832605c34347b Mon Sep 17 00:00:00 2001 From: threeandthreee Date: Wed, 12 Feb 2025 11:46:43 -0500 Subject: [PATCH 58/60] LADX: bomb as logical bush breaker #4636 --- worlds/ladx/LADXR/logic/requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py index fa01627a15c3..4e1fe03b096f 100644 --- a/worlds/ladx/LADXR/logic/requirements.py +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -253,7 +253,7 @@ def isConsumable(item) -> bool: class RequirementsSettings: def __init__(self, options): - self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG) + self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB) self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG) self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos From c799531105808c45849aa282168a3efa1a58f2e7 Mon Sep 17 00:00:00 2001 From: Matthew Wells <91291346+richarm4@users.noreply.github.com> Date: Wed, 12 Feb 2025 08:47:17 -0800 Subject: [PATCH 59/60] Docs: Add missing plural in faq (#4622) --- WebHostLib/static/assets/faq/en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/faq/en.md b/WebHostLib/static/assets/faq/en.md index e64535b42d03..96e526612be6 100644 --- a/WebHostLib/static/assets/faq/en.md +++ b/WebHostLib/static/assets/faq/en.md @@ -22,7 +22,7 @@ players to rely upon each other to complete their game. While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows players to randomize any of the supported games, and send items between them. This allows players of different -games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld. +games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds. Here is a list of our [Supported Games](https://archipelago.gg/games). ## Can I generate a single-player game with Archipelago? From efd5004330e2cb25bac094a2b78f370cacece635 Mon Sep 17 00:00:00 2001 From: JoshuaEagles Date: Wed, 12 Feb 2025 11:47:43 -0500 Subject: [PATCH 60/60] Docs: Update SA2B Linux and Steam Deck Setup Guide + Add Celeste 64 Linux Setup Guide (#4593) * Update Linux and Steam Deck setup guide for sa2b * Add Linux and Steam Deck setup guide for Celeste 64 --- worlds/celeste64/docs/guide_en.md | 8 +++++-- worlds/sa2b/docs/setup_en.md | 36 ++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/worlds/celeste64/docs/guide_en.md b/worlds/celeste64/docs/guide_en.md index 74ab94b913d1..87ebf09f755e 100644 --- a/worlds/celeste64/docs/guide_en.md +++ b/worlds/celeste64/docs/guide_en.md @@ -12,6 +12,12 @@ 1. Download the above release and extract it. +## Installation Procedures (Linux and Steam Deck) + +1. Download the above release and extract it. + +2. Add Celeste64.exe to Steam as a Non-Steam Game. In the properties for it on Steam, set it to use Proton as the compatibility tool. Launch the game through Steam in order to run it. + ## Joining a MultiWorld Game 1. Before launching the game, edit the `AP.json` file in the root of the Celeste 64 install. @@ -33,5 +39,3 @@ An Example `AP.json` file: "Password": "" } ``` - - diff --git a/worlds/sa2b/docs/setup_en.md b/worlds/sa2b/docs/setup_en.md index f32001a67827..c34e45ce9b51 100644 --- a/worlds/sa2b/docs/setup_en.md +++ b/worlds/sa2b/docs/setup_en.md @@ -5,7 +5,7 @@ - Sonic Adventure 2: Battle from: [Sonic Adventure 2: Battle Steam Store Page](https://store.steampowered.com/app/213610/Sonic_Adventure_2/) - The Battle DLC is required if you choose to add Chao Karate locations to the randomizer - SA Mod Manager from: [SA Mod Manager GitHub Releases Page](https://github.com/X-Hax/SA-Mod-Manager/releases) -- .NET Desktop Runtime 7.0 from: [.NET Desktop Runtime 7.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.9-windows-x64-installer) +- .NET Desktop Runtime 8.0 from: [.NET Desktop Runtime 8.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-8.0.12-windows-x64-installer) - Archipelago Mod for Sonic Adventure 2: Battle from: [Sonic Adventure 2: Battle Archipelago Randomizer Mod Releases Page](https://github.com/PoryGone/SA2B_Archipelago/releases/) @@ -36,27 +36,23 @@ 1. Install Sonic Adventure 2: Battle from Steam. -2. In the properties for Sonic Adventure 2 on Steam, force the use of Proton Experimental as the compatibility tool. - -3. Launch the game at least once without mods. - -4. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle. +2. Launch the game at least once without mods. -5. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool. +3. Create both a `/mods` directory and a `/SAManager` directory in the folder into which you installed Sonic Adventure 2: Battle. -6. Run SAModManager.exe from Steam once. It should produce an error popup for a missing dependency, close the error. +4. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path. -7. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks). +5. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is). -8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer). If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0). +6. Install SA Mod Manager as per [its instructions](https://github.com/X-Hax/SA-Mod-Manager/tree/master?tab=readme-ov-file). Specifically, extract SAModManager.exe file to the folder that Sonic Adventure 2: Battle is installed to. To launch it, add ``SAModManager.exe`` as a non-Steam game. In the properties on Steam for SA Mod Manager, set it to use Proton as the compatibility tool. -9. Right click the .NET 7 Desktop Runtime exe, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET 7 Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam. +7. Run SAModManager.exe from Steam once. It should produce an error popup saying you need .NET Desktop Runtime and ask you if you'd like to download it. Say yes and it will download through your browser. -6. Unpack the Archipelago Mod into this folder, so that `/mods/SA2B_Archipelago` is a valid path. +8. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks). -7. In the SA2B_Archipelago folder, copy the `APCpp.dll` file and paste it in the Sonic Adventure 2 install folder (where `sonic2app.exe` is). +9. Right click the .NET Desktop Runtime exe that was downloaded in step 6, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam. -8. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled. +10. Launch `SAModManager.exe` from Steam and make sure the SA2B_Archipelago mod is listed and enabled. Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rather than launching using the `Save & Play` button in SA Mod Manager. @@ -77,7 +73,7 @@ Note: Ensure that you launch Sonic Adventure 2 from Steam directly on Linux, rat ## Additional Options Some additional settings related to the Archipelago messages in game can be adjusted in the SAModManager if you select `Configure Mod` on the SA2B_Archipelago mod. This settings will be under a `General Settings` tab. - + - Message Display Count: This is the maximum number of Archipelago messages that can be displayed on screen at any given time. - Message Display Duration: This dictates how long Archipelago messages are displayed on screen (in seconds). - Message Font Size: The is the size of the font used to display the messages from Archipelago. @@ -94,7 +90,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop - "The following mods didn't load correctly: SA2B_Archipelago: DLL error - The specified module could not be found." - Make sure the `APCpp.dll` is in the same folder as the `sonic2app.exe`. (See Installation Procedures step 6) - + - "sonic2app.exe - Entry Point Not Found" - Make sure the `APCpp.dll` is up to date. Follow Installation Procedures step 6 to update the dll. @@ -116,7 +112,7 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop 1. Run the Launcher.exe which should be in the same folder as the your Sonic Adventure 2: Battle install. 2. Select the `Player` tab and reselect the controller for the player 1 input method. 3. Click the `Save settings and launch SONIC ADVENTURE 2` button. (Any mod manager settings will apply even if the game is launched this way rather than through the mod manager) - + - Game crashes after display logos. - This may be caused by a high monitor refresh rate. - Change the monitor refresh rate to 60 Hz [Change display refresh rate on Windows] (https://support.microsoft.com/en-us/windows/change-your-display-refresh-rate-in-windows-c8ea729e-0678-015c-c415-f806f04aae5a) @@ -125,13 +121,13 @@ If you wish to use the `SADX Music` option of the Randomizer, you must own a cop 2. Select the `Compatibility` tab. 3. Check the `Run this program in compatility mode for:` box and select Windows 7 in the drop down. 4. Click the `Apply` button. - + - No resolution options in the Launcher.exe. - In the `Graphics device` dropdown, select the device and display you plan to run the game on. The `Resolution` dropdown should populate once a graphics device is selected. - + - No music is playing in the game. - If you enabled an `SADX Music` option, then most likely the music data was not copied properly into the mod folder (See Additional Options for instructions). - + - Mission 1 is missing a texture in the stage select UI. - Most likely another mod is conflicting and overwriting the texture pack. It is recommeded to have the SA2B Archipelago mod load last in the mod manager.