From f25ef639f2d127bb991b6bf30913d1da832816c0 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Jun 2025 17:43:23 -0500 Subject: [PATCH 01/15] Launcher: Fix Cli Components when installed to a directory with a space (#5091) --- Launcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Launcher.py b/Launcher.py index 82326aacd7..5720012cf9 100644 --- a/Launcher.py +++ b/Launcher.py @@ -196,7 +196,8 @@ def get_exe(component: str | Component) -> Sequence[str] | None: def launch(exe, in_terminal=False): if in_terminal: if is_windows: - subprocess.Popen(['start', *exe], shell=True) + # intentionally using a window title with a space so it gets quoted and treated as a title + subprocess.Popen(["start", "Running Archipelago", *exe], shell=True) return elif is_linux: terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm') From ddb3240591feab473e76b1363fe4b45ff1110610 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Mon, 9 Jun 2025 08:58:08 -0400 Subject: [PATCH 02/15] KH2: Give warning when client has cached locations (#5000) * a * disconnect when connect to wrong slot * connection to the wrong seed fix * seed_name is always none --- worlds/kh2/Client.py | 58 ++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 96b406c72f..fcd27c4c3c 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -34,7 +34,7 @@ class KH2Context(CommonContext): self.growthlevel = None self.kh2connected = False self.kh2_finished_game = False - self.serverconneced = False + self.serverconnected = False self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()} self.location_name_to_data = {name: data for name, data, in all_locations.items()} self.kh2_data_package = {} @@ -47,6 +47,8 @@ class KH2Context(CommonContext): self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()} self.sending = [] + self.slot_name = None + self.disconnect_from_server = False # list used to keep track of locations+items player has. Used for disoneccting self.kh2_seed_save_cache = { "itemIndex": -1, @@ -185,11 +187,20 @@ class KH2Context(CommonContext): if password_requested and not self.password: await super(KH2Context, self).server_auth(password_requested) await self.get_username() - await self.send_connect() + # if slot name != first time login or previous name + # and seed name is none or saved seed name + if not self.slot_name and not self.kh2seedname: + await self.send_connect() + elif self.slot_name == self.auth and self.kh2seedname: + await self.send_connect() + else: + logger.info(f"You are trying to connect with data still cached in the client. Close client or connect to the correct slot: {self.slot_name}") + self.serverconnected = False + self.disconnect_from_server = True async def connection_closed(self): self.kh2connected = False - self.serverconneced = False + self.serverconnected = False if self.kh2seedname is not None and self.auth is not None: with open(self.kh2_seed_save_path_join, 'w') as f: f.write(json.dumps(self.kh2_seed_save, indent=4)) @@ -197,7 +208,8 @@ class KH2Context(CommonContext): async def disconnect(self, allow_autoreconnect: bool = False): self.kh2connected = False - self.serverconneced = False + self.serverconnected = False + self.locations_checked = [] if self.kh2seedname not in {None} and self.auth not in {None}: with open(self.kh2_seed_save_path_join, 'w') as f: f.write(json.dumps(self.kh2_seed_save, indent=4)) @@ -239,7 +251,15 @@ class KH2Context(CommonContext): def on_package(self, cmd: str, args: dict): if cmd == "RoomInfo": - self.kh2seedname = args['seed_name'] + if not self.kh2seedname: + self.kh2seedname = args['seed_name'] + elif self.kh2seedname != args['seed_name']: + self.disconnect_from_server = True + self.serverconnected = False + self.kh2connected = False + logger.info("Connection to the wrong seed, connect to the correct seed or close the client.") + return + self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json" self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path) @@ -338,7 +358,7 @@ class KH2Context(CommonContext): }, }, } - if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconneced: + if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconnected: self.kh2_seed_save_cache["itemIndex"] = start_index for item in args['items']: asyncio.create_task(self.give_item(item.item, item.location)) @@ -370,12 +390,14 @@ class KH2Context(CommonContext): if not self.kh2: self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") self.get_addresses() - +# except Exception as e: if self.kh2connected: self.kh2connected = False logger.info("Game is not open.") - self.serverconneced = True + + self.serverconnected = True + self.slot_name = self.auth def data_package_kh2_cache(self, loc_to_id, item_to_id): self.kh2_loc_name_to_id = loc_to_id @@ -930,7 +952,7 @@ def finishedGame(ctx: KH2Context): async def kh2_watcher(ctx: KH2Context): while not ctx.exit_event.is_set(): try: - if ctx.kh2connected and ctx.serverconneced: + if ctx.kh2connected and ctx.serverconnected: ctx.sending = [] await asyncio.create_task(ctx.checkWorldLocations()) await asyncio.create_task(ctx.checkLevels()) @@ -944,13 +966,19 @@ async def kh2_watcher(ctx: KH2Context): if ctx.sending: message = [{"cmd": 'LocationChecks', "locations": ctx.sending}] await ctx.send_msgs(message) - elif not ctx.kh2connected and ctx.serverconneced: - logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.") + elif not ctx.kh2connected and ctx.serverconnected: + logger.info("Game Connection lost. trying to reconnect.") ctx.kh2 = None - while not ctx.kh2connected and ctx.serverconneced: - await asyncio.sleep(15) - ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - ctx.get_addresses() + while not ctx.kh2connected and ctx.serverconnected: + try: + ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") + ctx.get_addresses() + logger.info("Game Connection Established.") + except Exception as e: + await asyncio.sleep(5) + if ctx.disconnect_from_server: + ctx.disconnect_from_server = False + await ctx.disconnect() except Exception as e: if ctx.kh2connected: ctx.kh2connected = False From a8c87ce54ba68007e5f8f291355ffeeeed408ca1 Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Mon, 9 Jun 2025 20:55:40 -0700 Subject: [PATCH 03/15] CI: Add GH_REPO environment variable to labeler (#5081) --- .github/workflows/label-pull-requests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/label-pull-requests.yml b/.github/workflows/label-pull-requests.yml index bc0f6999b6..4a7d403459 100644 --- a/.github/workflows/label-pull-requests.yml +++ b/.github/workflows/label-pull-requests.yml @@ -6,6 +6,8 @@ on: permissions: contents: read pull-requests: write +env: + GH_REPO: ${{ github.repository }} jobs: labeler: From 52b11083fe23a6fdbf01ee4cda1dd2bc97800006 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:52:47 -0400 Subject: [PATCH 04/15] KH2: Raise Exception for Misusing DonaldGoofyStatsanity Option (#4710) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/kh2/Client.py | 30 +++++++++++++++++++++++++----- worlds/kh2/__init__.py | 11 ++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index fcd27c4c3c..5a26231c0c 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -515,23 +515,38 @@ class KH2Context(CommonContext): async def give_item(self, item, location): try: - # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites - #sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts + # sleep so we can get the datapackage and not miss any items that were sent to us while we didnt have our item id dicts while not self.lookup_id_to_item: await asyncio.sleep(0.5) itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] - # itemcode = self.kh2_item_name_to_id[itemname] if itemdata.ability: if location in self.all_weapon_location_id: return + # growth have reserved ability slots because of how the goa handles them if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}: self.kh2_seed_save_cache["AmountInvo"]["Growth"][itemname] += 1 return if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Ability"]: self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname] = [] - # appending the slot that the ability should be in + # appending the slot that the ability should be in + # abilities have a limit amount of slots. + # we start from the back going down to not mess with stuff. + # Front of Invo + # Sora: Save+24F0+0x54 : 0x2546 + # Donald: Save+2604+0x54 : 0x2658 + # Goofy: Save+2718+0x54 : 0x276C + # Back of Invo. Sora has 6 ability slots that are reserved + # Sora: Save+24F0+0x54+0x92 : 0x25D8 + # Donald: Save+2604+0x54+0x9C : 0x26F4 + # Goofy: Save+2718+0x54+0x9C : 0x2808 + # seed has 2 scans in sora's abilities + # recieved second scan + # if len(seed_save(Scan:[ability slot 52]) < (2)amount of that ability they should have from slot data + # ability_slot = back of inventory that isnt taken + # add ability_slot to seed_save(Scan[]) so now its Scan:[ability slot 52,50] + # decrease back of inventory since its ability_slot is already taken if len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ self.AbilityQuantityDict[itemname]: if itemname in self.sora_ability_set: @@ -550,18 +565,21 @@ class KH2Context(CommonContext): if ability_slot in self.front_ability_slots: self.front_ability_slots.remove(ability_slot) + # if itemdata in {bitmask} all the forms,summons and a few other things are bitmasks elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: # if memaddr is in a bitmask location in memory if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: self.kh2_seed_save_cache["AmountInvo"]["Bitmask"].append(itemname) + # if itemdata in {magic} elif itemdata.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}: - # if memaddr is in magic addresses self.kh2_seed_save_cache["AmountInvo"]["Magic"][itemname] += 1 + # equipment is a list instead of dict because you can only have 1 currently elif itemname in self.all_equipment: self.kh2_seed_save_cache["AmountInvo"]["Equipment"].append(itemname) + # weapons are done differently since you can only have one and has to check it differently elif itemname in self.all_weapons: if itemname in self.keyblade_set: self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Sora"].append(itemname) @@ -570,9 +588,11 @@ class KH2Context(CommonContext): else: self.kh2_seed_save_cache["AmountInvo"]["Weapon"]["Goofy"].append(itemname) + # TODO: this can just be removed and put into the else below it elif itemname in self.stat_increase_set: self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][itemname] += 1 else: + # "normal" items. They have a unique byte reserved for how many they have if itemname in self.kh2_seed_save_cache["AmountInvo"]["Amount"]: self.kh2_seed_save_cache["AmountInvo"]["Amount"][itemname] += 1 else: diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index defb285d50..19c2aee61f 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -277,9 +277,7 @@ class KH2World(World): if self.options.FillerItemsLocal: for item in filler_items: self.options.local_items.value.add(item) - # By imitating remote this doesn't have to be plandoded filler anymore - # for location in {LocationName.JunkMedal, LocationName.JunkMedal}: - # self.plando_locations[location] = random_stt_item + if not self.options.SummonLevelLocationToggle: self.total_locations -= 6 @@ -400,6 +398,8 @@ class KH2World(World): # plando goofy get bonuses goofy_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in Goofy_Checks.keys() if Goofy_Checks[location].yml != "Keyblade"] + if len(goofy_get_bonus_location_pool) > len(self.goofy_get_bonus_abilities): + raise Exception(f"Too little abilities to fill goofy get bonus locations for player {self.player_name}.") for location in goofy_get_bonus_location_pool: self.random.choice(self.goofy_get_bonus_abilities) random_ability = self.random.choice(self.goofy_get_bonus_abilities) @@ -416,11 +416,12 @@ class KH2World(World): random_ability = self.random.choice(self.donald_weapon_abilities) location.place_locked_item(random_ability) self.donald_weapon_abilities.remove(random_ability) - + # if option is turned off if not self.options.DonaldGoofyStatsanity: - # plando goofy get bonuses donald_get_bonus_location_pool = [self.multiworld.get_location(location, self.player) for location in Donald_Checks.keys() if Donald_Checks[location].yml != "Keyblade"] + if len(donald_get_bonus_location_pool) > len(self.donald_get_bonus_abilities): + raise Exception(f"Too little abilities to fill donald get bonus locations for player {self.player_name}.") for location in donald_get_bonus_location_pool: random_ability = self.random.choice(self.donald_get_bonus_abilities) location.place_locked_item(random_ability) From aecbb2ab0259e8683dc848b538ac2ab7d5ee1fb9 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 13 Jun 2025 05:28:58 -0500 Subject: [PATCH 05/15] fix saving princess's use of subprocess helpers (#5103) --- worlds/saving_princess/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/saving_princess/__init__.py b/worlds/saving_princess/__init__.py index 4109f356fd..f731012abc 100644 --- a/worlds/saving_princess/__init__.py +++ b/worlds/saving_princess/__init__.py @@ -12,7 +12,7 @@ from .Constants import * def launch_client(*args: str): from .Client import launch - launch_subprocess(launch(*args), name=CLIENT_NAME) + launch_subprocess(launch, name=CLIENT_NAME, args=args) components.append( From 8c6327d024e6d18503b018b3555c0f24d88b13a6 Mon Sep 17 00:00:00 2001 From: qwint Date: Fri, 13 Jun 2025 14:56:09 -0500 Subject: [PATCH 06/15] LTTP/SDV: use .name when appropriate in subtests (#5107) --- worlds/alttp/test/options/test_dungeon_fill.py | 4 ++-- .../test/regions/TestEntranceClassifications.py | 2 +- .../stardew_valley/test/regions/TestRegionConnections.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/alttp/test/options/test_dungeon_fill.py b/worlds/alttp/test/options/test_dungeon_fill.py index 17501b65d8..4a0d30f7d9 100644 --- a/worlds/alttp/test/options/test_dungeon_fill.py +++ b/worlds/alttp/test/options/test_dungeon_fill.py @@ -38,7 +38,7 @@ class DungeonFillTestBase(TestCase): def test_original_dungeons(self): self.generate_with_options(DungeonItem.option_original_dungeon) for location in self.multiworld.get_filled_locations(): - with (self.subTest(location=location)): + with (self.subTest(location_name=location.name)): if location.parent_region.dungeon is None: self.assertIs(location.item.dungeon, None) else: @@ -52,7 +52,7 @@ class DungeonFillTestBase(TestCase): def test_own_dungeons(self): self.generate_with_options(DungeonItem.option_own_dungeons) for location in self.multiworld.get_filled_locations(): - with self.subTest(location=location): + with self.subTest(location_name=location.name): if location.parent_region.dungeon is None: self.assertIs(location.item.dungeon, None) else: diff --git a/worlds/stardew_valley/test/regions/TestEntranceClassifications.py b/worlds/stardew_valley/test/regions/TestEntranceClassifications.py index 43a7090482..4bc13cb51c 100644 --- a/worlds/stardew_valley/test/regions/TestEntranceClassifications.py +++ b/worlds/stardew_valley/test/regions/TestEntranceClassifications.py @@ -11,7 +11,7 @@ class EntranceRandomizationAssertMixin: non_progression_connections = [connection for connection in all_connections.values() if RandomizationFlag.BIT_NON_PROGRESSION in connection.flag] for non_progression_connections in non_progression_connections: - with self.subTest(connection=non_progression_connections): + with self.subTest(connection=non_progression_connections.name): self.assert_can_reach_entrance(non_progression_connections.name) diff --git a/worlds/stardew_valley/test/regions/TestRegionConnections.py b/worlds/stardew_valley/test/regions/TestRegionConnections.py index 42a2e36124..f20ef7943c 100644 --- a/worlds/stardew_valley/test/regions/TestRegionConnections.py +++ b/worlds/stardew_valley/test/regions/TestRegionConnections.py @@ -12,14 +12,14 @@ from ...regions.regions import create_all_regions, create_all_connections class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase): def test_region_exits_lead_somewhere(self): for region in vanilla_data.regions_with_ginger_island_by_name.values(): - with self.subTest(region=region): + with self.subTest(region=region.name): for exit_ in region.exits: self.assertIn(exit_, vanilla_data.connections_with_ginger_island_by_name, f"{region.name} is leading to {exit_} but it does not exist.") def test_connection_lead_somewhere(self): for connection in vanilla_data.connections_with_ginger_island_by_name.values(): - with self.subTest(connection=connection): + with self.subTest(connection=connection.name): self.assertIn(connection.destination, vanilla_data.regions_with_ginger_island_by_name, f"{connection.name} is leading to {connection.destination} but it does not exist.") @@ -27,14 +27,14 @@ class TestVanillaRegionsConnectionsWithGingerIsland(unittest.TestCase): class TestVanillaRegionsConnectionsWithoutGingerIsland(unittest.TestCase): def test_region_exits_lead_somewhere(self): for region in vanilla_data.regions_without_ginger_island_by_name.values(): - with self.subTest(region=region): + with self.subTest(region=region.name): for exit_ in region.exits: self.assertIn(exit_, vanilla_data.connections_without_ginger_island_by_name, f"{region.name} is leading to {exit_} but it does not exist.") def test_connection_lead_somewhere(self): for connection in vanilla_data.connections_without_ginger_island_by_name.values(): - with self.subTest(connection=connection): + with self.subTest(connection=connection.name): self.assertIn(connection.destination, vanilla_data.regions_without_ginger_island_by_name, f"{connection.name} is leading to {connection.destination} but it does not exist.") From 0ad4527719c60caacbb6b1777b256556c7ad52d9 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:01:19 -0400 Subject: [PATCH 07/15] SA2B: Logic Fixes (#5095) - Fixed King Boom Boo being able to appear in multiple boss gates - `Final Rush - 16 Animals (Expert)` no longer requires `Sonic - Bounce Bracelet` - `Dry Lagoon - 5 (Standard)` now requires `Rouge - Pick Nails` - `Sand Ocean - Extra Life Box 2 (Standard/Hard/Expert)` no longer requires `Eggman - Jet Engine` - `Security Hall - 8 Animals (Expert)` no longer requires `Rouge - Pick Nails` - `Sky Rail - Item Box 8 (Standard)` now requires `Shadow - Air Shoes` and `Shadow - Mystic Melody` - `Cosmic Wall - Chao Key 1 (Standard/Hard/Expert)` no longer requires `Eggman - Mystic Melody` - `Cannon's Core - Pipe 2 (Expert)` no longer requires `Tails - Booster` - `Cannon's Core - Gold Beetle` no longer requires `Tails - Booster` nor `Knuckles - Hammer Gloves` --- worlds/sa2b/GateBosses.py | 13 ++++++++----- worlds/sa2b/Rules.py | 33 +++++++++------------------------ 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/worlds/sa2b/GateBosses.py b/worlds/sa2b/GateBosses.py index 9e1a81bae9..02e089359b 100644 --- a/worlds/sa2b/GateBosses.py +++ b/worlds/sa2b/GateBosses.py @@ -1,6 +1,7 @@ import typing from BaseClasses import MultiWorld +from Options import OptionError from worlds.AutoWorld import World from .Names import LocationName @@ -99,8 +100,9 @@ def get_gate_bosses(world: World): pass if boss in plando_bosses: - # TODO: Raise error here. Duplicates not allowed - pass + raise OptionError(f"Invalid input for option `plando_bosses`: " + f"No Duplicate Bosses permitted ({boss}) - for " + f"{world.player_name}") plando_bosses[boss_num] = boss @@ -108,13 +110,14 @@ def get_gate_bosses(world: World): available_bosses.remove(boss) for x in range(world.options.number_of_level_gates): - if ("king boom boo" not in selected_bosses) and ("king boom boo" not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5: - available_bosses.extend(gate_bosses_with_requirements_table) + if (10 not in selected_bosses) and (king_boom_boo not in available_bosses) and ((x + 1) / world.options.number_of_level_gates) > 0.5: + available_bosses.extend(gate_bosses_with_requirements_table.keys()) world.random.shuffle(available_bosses) chosen_boss = available_bosses[0] if plando_bosses[x] != "None": - available_bosses.append(plando_bosses[x]) + if plando_bosses[x] not in available_bosses: + available_bosses.append(plando_bosses[x]) chosen_boss = plando_bosses[x] selected_bosses.append(all_gate_bosses_table[chosen_boss]) diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index 9019a5b033..a7ea9becb1 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -324,7 +324,8 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla add_rule_safe(multiworld, LocationName.iron_gate_5, player, lambda state: state.has(ItemName.eggman_large_cannon, player)) add_rule_safe(multiworld, LocationName.dry_lagoon_5, player, - lambda state: state.has(ItemName.rouge_treasure_scope, player)) + lambda state: state.has(ItemName.rouge_pick_nails, player) and + state.has(ItemName.rouge_treasure_scope, player)) add_rule_safe(multiworld, LocationName.sand_ocean_5, player, lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule_safe(multiworld, LocationName.egg_quarters_5, player, @@ -407,8 +408,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player), - lambda state: state.has(ItemName.eggman_mystic_melody, player) and - state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player), lambda state: state.has(ItemName.tails_booster, player) and @@ -1402,8 +1402,6 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla state.has(ItemName.eggman_large_cannon, player))) add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player), lambda state: state.has(ItemName.rouge_treasure_scope, player)) - add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player), lambda state: (state.has(ItemName.rouge_mystic_melody, player) and state.has(ItemName.rouge_treasure_scope, player))) @@ -1724,6 +1722,9 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.white_jungle_itembox_8, player), lambda state: state.has(ItemName.shadow_air_shoes, player)) + add_rule(multiworld.get_location(LocationName.sky_rail_itembox_8, player), + lambda state: (state.has(ItemName.shadow_air_shoes, player) and + state.has(ItemName.shadow_mystic_melody, player))) add_rule(multiworld.get_location(LocationName.mad_space_itembox_8, player), lambda state: state.has(ItemName.rouge_iron_boots, player)) add_rule(multiworld.get_location(LocationName.cosmic_wall_itembox_8, player), @@ -2308,8 +2309,7 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player: lambda state: state.has(ItemName.tails_booster, player)) add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player), - lambda state: state.has(ItemName.eggman_mystic_melody, player) and - state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player), lambda state: state.has(ItemName.tails_booster, player) and @@ -2980,8 +2980,6 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player: state.has(ItemName.eggman_jet_engine, player))) add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player), lambda state: state.has(ItemName.rouge_treasure_scope, player)) - add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player), lambda state: (state.has(ItemName.rouge_mystic_melody, player) and state.has(ItemName.rouge_treasure_scope, player))) @@ -3593,8 +3591,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe lambda state: state.has(ItemName.tails_booster, player)) add_rule(multiworld.get_location(LocationName.cosmic_wall_chao_1, player), - lambda state: state.has(ItemName.eggman_mystic_melody, player) and - state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.cannon_core_chao_1, player), lambda state: state.has(ItemName.eggman_jet_engine, player) and @@ -3643,9 +3640,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe add_rule(multiworld.get_location(LocationName.cosmic_wall_pipe_2, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) - add_rule(multiworld.get_location(LocationName.cannon_core_pipe_2, player), - lambda state: state.has(ItemName.tails_booster, player)) - add_rule(multiworld.get_location(LocationName.prison_lane_pipe_3, player), lambda state: state.has(ItemName.tails_bazooka, player)) add_rule(multiworld.get_location(LocationName.mission_street_pipe_3, player), @@ -3771,10 +3765,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe add_rule(multiworld.get_location(LocationName.cosmic_wall_beetle, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) - add_rule(multiworld.get_location(LocationName.cannon_core_beetle, player), - lambda state: state.has(ItemName.tails_booster, player) and - state.has(ItemName.knuckles_hammer_gloves, player)) - # Animal Upgrade Requirements if world.options.animalsanity: add_rule(multiworld.get_location(LocationName.hidden_base_animal_2, player), @@ -3839,8 +3829,7 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe add_rule(multiworld.get_location(LocationName.weapons_bed_animal_8, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.security_hall_animal_8, player), - lambda state: state.has(ItemName.rouge_pick_nails, player) and - state.has(ItemName.rouge_iron_boots, player)) + lambda state: state.has(ItemName.rouge_iron_boots, player)) add_rule(multiworld.get_location(LocationName.cosmic_wall_animal_8, player), lambda state: state.has(ItemName.eggman_jet_engine, player)) @@ -3976,8 +3965,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe state.has(ItemName.tails_bazooka, player)) add_rule(multiworld.get_location(LocationName.crazy_gadget_animal_16, player), lambda state: state.has(ItemName.sonic_flame_ring, player)) - add_rule(multiworld.get_location(LocationName.final_rush_animal_16, player), - lambda state: state.has(ItemName.sonic_bounce_bracelet, player)) add_rule(multiworld.get_location(LocationName.final_chase_animal_17, player), lambda state: state.has(ItemName.shadow_flame_ring, player)) @@ -4035,8 +4022,6 @@ def set_mission_upgrade_rules_expert(multiworld: MultiWorld, world: World, playe lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.dry_lagoon_lifebox_2, player), lambda state: state.has(ItemName.rouge_treasure_scope, player)) - add_rule(multiworld.get_location(LocationName.sand_ocean_lifebox_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) add_rule(multiworld.get_location(LocationName.egg_quarters_lifebox_2, player), lambda state: state.has(ItemName.rouge_treasure_scope, player)) From 068a7573737078d413cdd0fe1ea4c94bc2903821 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:29:06 -0400 Subject: [PATCH 08/15] Item Plando: Fix `count` value (#5101) --- Fill.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index d0a42c07eb..87d6b02e09 100644 --- a/Fill.py +++ b/Fill.py @@ -937,13 +937,16 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo count = block.count if not count: - count = len(new_block.items) + count = (min(len(new_block.items), len(new_block.resolved_locations)) + if new_block.resolved_locations else len(new_block.items)) if isinstance(count, int): count = {"min": count, "max": count} if "min" not in count: count["min"] = 0 if "max" not in count: - count["max"] = len(new_block.items) + count["max"] = (min(len(new_block.items), len(new_block.resolved_locations)) + if new_block.resolved_locations else len(new_block.items)) + new_block.count = count plando_blocks[player].append(new_block) From e83e178b63272d2a1ec96dd2ae04dbae64c3f737 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Fri, 13 Jun 2025 20:29:23 -0400 Subject: [PATCH 09/15] Stardew Valley: Fix 3 Logic Issues (#5094) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/stardew_valley/data/bundle_data.py | 10 ++++---- worlds/stardew_valley/logic/logic.py | 24 +++++++++++-------- .../strings/monster_drop_names.py | 5 ---- .../stardew_valley/test/rules/TestFishing.py | 9 +++---- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 3a5523ecdd..3f289d33cd 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -271,11 +271,11 @@ solar_essence = BundleItem(Loot.solar_essence) void_essence = BundleItem(Loot.void_essence) petrified_slime = BundleItem(Mineral.petrified_slime) -blue_slime_egg = BundleItem(Loot.blue_slime_egg) -red_slime_egg = BundleItem(Loot.red_slime_egg) -purple_slime_egg = BundleItem(Loot.purple_slime_egg) -green_slime_egg = BundleItem(Loot.green_slime_egg) -tiger_slime_egg = BundleItem(Loot.tiger_slime_egg, source=BundleItem.Sources.island) +blue_slime_egg = BundleItem(AnimalProduct.slime_egg_blue) +red_slime_egg = BundleItem(AnimalProduct.slime_egg_red) +purple_slime_egg = BundleItem(AnimalProduct.slime_egg_purple) +green_slime_egg = BundleItem(AnimalProduct.slime_egg_green) +tiger_slime_egg = BundleItem(AnimalProduct.slime_egg_tiger, source=BundleItem.Sources.island) cherry_bomb = BundleItem(Bomb.cherry_bomb, 5) bomb = BundleItem(Bomb.bomb, 2) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index 716dd06571..42bfb9cc26 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -168,15 +168,16 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)), AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond), AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(), - AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg + AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep), AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime), AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3), AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6), AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9), - AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond), - AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet... - AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet. + AnimalProduct.slime_egg_tiger: self.can_fish_pond(Fish.lionfish, *(Forageable.ginger, Fruit.pineapple, Fruit.mango)) & self.time.has_lived_months(12) & + self.building.has_building(Building.slime_hutch) & self.monster.can_kill(Monster.tiger_slime), + AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet... + AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet. AnimalProduct.egg_starter: self.logic.false_, # It could be purchased at the Desert Festival, but festival logic is quite a mess, so not considering it yet... AnimalProduct.golden_egg_starter: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)), AnimalProduct.void_egg_starter: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)), @@ -233,7 +234,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), # Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton), Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe), - Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe), + Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe) & self.received("Open Professor Snail Cave"), Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut), Fossil.fossilized_spine: self.fishing.can_fish_at(Region.dig_site), Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper), @@ -288,9 +289,9 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))), MetalBar.radioactive: self.can_smelt(Ore.radioactive), Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), - Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron), - Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold), - Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), + Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.gold), + Ore.iridium: self.count(2, *(self.mine.can_mine_in_the_skull_cavern(), self.can_fish_pond(Fish.super_cucumber), self.tool.has_tool(Tool.pan, ToolMaterial.iridium))), + Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron), Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room), RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100), RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), @@ -381,5 +382,8 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin def can_use_obelisk(self, obelisk: str) -> StardewRule: return self.region.can_reach(Region.farm) & self.received(obelisk) - def can_fish_pond(self, fish: str) -> StardewRule: - return self.building.has_building(Building.fish_pond) & self.has(fish) + def can_fish_pond(self, fish: str, *items: str) -> StardewRule: + rule = self.building.has_building(Building.fish_pond) & self.has(fish) + if items: + rule = rule & self.has_all(*items) + return rule diff --git a/worlds/stardew_valley/strings/monster_drop_names.py b/worlds/stardew_valley/strings/monster_drop_names.py index df2cacf0c6..8612b3c7b5 100644 --- a/worlds/stardew_valley/strings/monster_drop_names.py +++ b/worlds/stardew_valley/strings/monster_drop_names.py @@ -1,9 +1,4 @@ class Loot: - blue_slime_egg = "Blue Slime Egg" - red_slime_egg = "Red Slime Egg" - purple_slime_egg = "Purple Slime Egg" - green_slime_egg = "Green Slime Egg" - tiger_slime_egg = "Tiger Slime Egg" slime = "Slime" bug_meat = "Bug Meat" bat_wing = "Bat Wing" diff --git a/worlds/stardew_valley/test/rules/TestFishing.py b/worlds/stardew_valley/test/rules/TestFishing.py index 3649592301..22e6321a7a 100644 --- a/worlds/stardew_valley/test/rules/TestFishing.py +++ b/worlds/stardew_valley/test/rules/TestFishing.py @@ -8,7 +8,7 @@ class TestNeedRegionToCatchFish(SVTestBase): SeasonRandomization.internal_name: SeasonRandomization.option_disabled, ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, SkillProgression.internal_name: SkillProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_progressive, Fishsanity.internal_name: Fishsanity.option_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, @@ -18,7 +18,7 @@ class TestNeedRegionToCatchFish(SVTestBase): fish_and_items = { Fish.crimsonfish: ["Beach Bridge"], Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"], - Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades + Fish.woodskip: ["Progressive Axe", "Progressive Axe", "Progressive Weapon"], # For the ores to get the axe upgrades Fish.mutant_carp: ["Rusty Key"], Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"], Fish.lionfish: ["Boat Repair"], @@ -26,8 +26,8 @@ class TestNeedRegionToCatchFish(SVTestBase): Fish.stingray: ["Boat Repair", "Island Resort"], Fish.ghostfish: ["Progressive Weapon"], Fish.stonefish: ["Progressive Weapon"], - Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"], - Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"], + Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe"], + Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon", "Progressive Pickaxe", "Progressive Pickaxe", "Progressive Pickaxe"], Fish.sandfish: ["Bus Repair"], Fish.scorpion_carp: ["Desert Obelisk"], # Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary @@ -37,6 +37,7 @@ class TestNeedRegionToCatchFish(SVTestBase): Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], } + self.collect("Progressive Fishing Rod", 4) self.original_state = self.multiworld.state.copy() for fish in fish_and_items: with self.subTest(f"Region rules for {fish}"): From 2ff611167a4415f2d06b6904434e814cf6595174 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 14 Jun 2025 12:21:25 +0200 Subject: [PATCH 10/15] =?UTF-8?q?ALTTP:=20Fix=20take=5Fany=20leaving=20a?= =?UTF-8?q?=20placed=20item=20in=20the=20multiworld=20itempool=C2=A0#5108?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/alttp/ItemPool.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 57ad01b9e4..9f1a58e546 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -548,10 +548,12 @@ def set_up_take_anys(multiworld, world, player): old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots) multiworld.shops.append(old_man_take_any.shop) - swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword'] - if swords: - sword = multiworld.random.choice(swords) - multiworld.itempool.remove(sword) + sword_indices = [ + index for index, item in enumerate(multiworld.itempool) if item.player == player and item.type == 'Sword' + ] + if sword_indices: + sword_index = multiworld.random.choice(sword_indices) + sword = multiworld.itempool.pop(sword_index) multiworld.itempool.append(item_factory('Rupees (20)', world)) old_man_take_any.shop.add_inventory(0, sword.name, 0, 0) loc_name = "Old Man Sword Cave" From 27a67705692e5abafbfc5dbd5753feb81294855c Mon Sep 17 00:00:00 2001 From: Louis M Date: Sat, 14 Jun 2025 07:17:33 -0400 Subject: [PATCH 11/15] Aquaria: Fixing open waters urns not breakable with nature forms logic bug (#5072) * Fixing open waters urns not breakable with nature forms logic bug * Using list in comprehension only when useful * Replacing damaging items by a constant * Removing comprehension list creating from lambda --- worlds/aquaria/Regions.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 40170e0c32..3436374ac7 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -4,7 +4,7 @@ Date: Fri, 15 Mar 2024 18:41:40 +0000 Description: Used to manage Regions in the Aquaria game multiworld randomizer """ -from typing import Dict, Optional +from typing import Dict, Optional, Iterable from BaseClasses import MultiWorld, Region, Entrance, Item, ItemClassification, CollectionState from .Items import AquariaItem, ItemNames from .Locations import AquariaLocations, AquariaLocation, AquariaLocationNames @@ -34,10 +34,15 @@ def _has_li(state: CollectionState, player: int) -> bool: return state.has(ItemNames.LI_AND_LI_SONG, player) -def _has_damaging_item(state: CollectionState, player: int) -> bool: - """`player` in `state` has the shield song item""" - return state.has_any({ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, ItemNames.LI_AND_LI_SONG, - ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, ItemNames.BABY_BLASTER}, player) +DAMAGING_ITEMS:Iterable[str] = [ + ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM, + ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA, + ItemNames.BABY_BLASTER +] + +def _has_damaging_item(state: CollectionState, player: int, damaging_items:Iterable[str] = DAMAGING_ITEMS) -> bool: + """`player` in `state` has the an item that do damage other than the ones in `to_remove`""" + return state.has_any(damaging_items, player) def _has_energy_attack_item(state: CollectionState, player: int) -> bool: @@ -566,9 +571,11 @@ class AquariaRegions: self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_turtle, lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_one_way_regions(self.openwater_tr_turtle, self.openwater_tr) + damaging_items_minus_nature_form = [item for item in DAMAGING_ITEMS if item != ItemNames.NATURE_FORM] self.__connect_one_way_regions(self.openwater_tr, self.openwater_tr_urns, lambda state: _has_bind_song(state, self.player) or - _has_damaging_item(state, self.player)) + _has_damaging_item(state, self.player, + damaging_items_minus_nature_form)) self.__connect_regions(self.openwater_tr, self.openwater_br) self.__connect_regions(self.openwater_tr, self.mithalas_city) self.__connect_regions(self.openwater_tr, self.veil_b) From 3b72140435d4d587a60865ea5e325f5b6aa1d950 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:26:22 -0400 Subject: [PATCH 12/15] Shivers: Fix get_pre_fill_items (#5113) --- worlds/shivers/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index 3430a5a02d..dd941b9212 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -261,13 +261,13 @@ class ShiversWorld(World): data.type == ItemType.POT_DUPLICATE] elif self.options.full_pots == "complete": return [self.create_item(name) for name, data in item_table.items() if - data.type == ItemType.POT_COMPELTE_DUPLICATE] + data.type == ItemType.POT_COMPLETE_DUPLICATE] else: pool = [] pieces = [self.create_item(name) for name, data in item_table.items() if data.type == ItemType.POT_DUPLICATE] complete = [self.create_item(name) for name, data in item_table.items() if - data.type == ItemType.POT_COMPELTE_DUPLICATE] + data.type == ItemType.POT_COMPLETE_DUPLICATE] for i in range(10): if self.pot_completed_list[i] == 0: pool.append(pieces[i]) From ecb739ce96716f83d128648d0350df69b5aae7eb Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:26:58 -0400 Subject: [PATCH 13/15] Plando Items: Fix Location Groups Unfolding (#5099) --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 87d6b02e09..9d460e49c4 100644 --- a/Fill.py +++ b/Fill.py @@ -923,9 +923,9 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo if isinstance(locations, str): locations = [locations] - locations_from_groups: list[str] = [] resolved_locations: list[Location] = [] for target_player in worlds: + locations_from_groups: list[str] = [] world_locations = multiworld.get_unfilled_locations(target_player) for group in multiworld.worlds[target_player].location_name_groups: if group in locations: From aa9e6175108afb16caec2411486e2f1a054ae4a6 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:27:22 -0400 Subject: [PATCH 14/15] DS3: Apply Rules to Non-Randomized Locations (#5106) --- worlds/dark_souls_3/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 94150faf05..6584ccec87 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -75,6 +75,13 @@ class DarkSouls3World(World): """The pool of all items within this particular world. This is a subset of `self.multiworld.itempool`.""" + missable_dupe_prog_locs: Set[str] = {"PC: Storm Ruler - Siegward", + "US: Pyromancy Flame - Cornyx", + "US: Tower Key - kill Irina"} + """Locations whose vanilla item is a missable duplicate of a non-missable progression item. + If vanilla, these locations shouldn't be expected progression, so they aren't created and don't get rules. + """ + def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) self.all_excluded_locations = set() @@ -258,10 +265,7 @@ class DarkSouls3World(World): new_location.progress_type = LocationProgressType.EXCLUDED else: # Don't allow missable duplicates of progression items to be expected progression. - if location.name in {"PC: Storm Ruler - Siegward", - "US: Pyromancy Flame - Cornyx", - "US: Tower Key - kill Irina"}: - continue + if location.name in self.missable_dupe_prog_locs: continue # Replace non-randomized items with events that give the default item event_item = ( @@ -1286,8 +1290,9 @@ class DarkSouls3World(World): data = location_dictionary[location] if data.dlc and not self.options.enable_dlc: continue if data.ngp and not self.options.enable_ngp: continue + # Don't add rules to missable duplicates of progression items + if location in self.missable_dupe_prog_locs and not self._is_location_available(location): continue - if not self._is_location_available(location): continue if isinstance(rule, str): assert item_dictionary[rule].classification == ItemClassification.progression rule = lambda state, item=rule: state.has(item, self.player) From ec5b4e704f8167dd262579120a9aa99d746ab04d Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:28:02 -0400 Subject: [PATCH 15/15] Plando Items: Better Warning for Nonexisting Worlds (#5112) --- Fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fill.py b/Fill.py index 9d460e49c4..abdad44070 100644 --- a/Fill.py +++ b/Fill.py @@ -890,7 +890,7 @@ def parse_planned_blocks(multiworld: MultiWorld) -> dict[int, list[PlandoItemBlo worlds = set() for listed_world in target_world: if listed_world not in world_name_lookup: - failed(f"Cannot place item to {target_world}'s world as that world does not exist.", + failed(f"Cannot place item to {listed_world}'s world as that world does not exist.", block.force) continue worlds.add(world_name_lookup[listed_world])