From 139f222b920055fa8902a70969ba80edf89358d3 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 18 May 2024 02:54:56 -0500 Subject: [PATCH] fix animal duplication bug --- worlds/kdl3/__init__.py | 20 ++++++++++++- worlds/kdl3/data/kdl3_basepatch.bsdiff4 | Bin 2618 -> 2665 bytes worlds/kdl3/names/animal_friend_spawns.py | 11 ++++++++ worlds/kdl3/rules.py | 9 +++++- worlds/kdl3/src/kdl3_basepatch.asm | 33 ++++++++++++++++++++++ worlds/kdl3/test/test_shuffles.py | 17 +++++++++++ 6 files changed, 88 insertions(+), 2 deletions(-) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 8ad889fd3b..38a3e47809 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -9,7 +9,7 @@ from .items import item_table, item_names, copy_ability_table, animal_friend_tab trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\ lookup_item_to_id from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations -from .names.animal_friend_spawns import animal_friend_spawns +from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive from .regions import create_levels, default_levels from .options import KDL3Options, kdl3_option_groups @@ -213,6 +213,24 @@ class KDL3World(World): self.collect(allstate, self.create_item(item)) self.random.shuffle(locations) fill_restrictive(self.multiworld, allstate, locations, items, True, True) + + # Need to ensure all of these are unique items, and replace them if they aren't + for spawns in problematic_sets: + placed = [self.get_location(spawn).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + if len(placed_names) != len(placed): + # have a duplicate + animals = [] + for spawn in spawns: + spawn_location = self.get_location(spawn) + if spawn_location.item.name not in animals: + animals.append(spawn_location.item.name) + else: + new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn", + "ChuChu Spawn", "Nago Spawn", "Pitch Spawn"] + if x not in placed_names]) + spawn_location.item = self.create_item(new_animal) + # logically, this should be sound pre-ER. May need to adjust around it with ER in the future else: animal_friends = animal_friend_spawns.copy() for animal in animal_friends: diff --git a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 index d16fef7b7f70d3cc82aa114748bdfaf397d0ee5b..369e91ec3c373e4d6ddbcf69ca796cde263ecf17 100644 GIT binary patch literal 2665 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 diff --git a/worlds/kdl3/names/animal_friend_spawns.py b/worlds/kdl3/names/animal_friend_spawns.py index 4520cf1438..5c1ba39697 100644 --- a/worlds/kdl3/names/animal_friend_spawns.py +++ b/worlds/kdl3/names/animal_friend_spawns.py @@ -1,3 +1,5 @@ +from typing import List + grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu @@ -197,3 +199,12 @@ animal_friend_spawns = { iceberg_6_a5: "ChuChu Spawn", iceberg_6_a6: "Nago Spawn", } + +problematic_sets: List[List[str]] = [ + # Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not. + [ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3], + [sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3], + [cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3], + [iceberg_6_a1, iceberg_6_a2, iceberg_6_a3], + [iceberg_6_a4, iceberg_6_a5, iceberg_6_a6] +] diff --git a/worlds/kdl3/rules.py b/worlds/kdl3/rules.py index b7a141b718..a08e99257e 100644 --- a/worlds/kdl3/rules.py +++ b/worlds/kdl3/rules.py @@ -1,5 +1,5 @@ from worlds.generic.Rules import set_rule, add_rule -from .names import location_name, enemy_abilities +from .names import location_name, enemy_abilities, animal_friend_spawns from .locations import location_table from .options import GoalSpeed import typing @@ -302,6 +302,13 @@ def set_rules(world: "KDL3World") -> None: set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) + # animal friend rules + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a2, world.player), + 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)) + for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 5 Boss - Purified"], diff --git a/worlds/kdl3/src/kdl3_basepatch.asm b/worlds/kdl3/src/kdl3_basepatch.asm index dc0c719398..2ba56f784a 100644 --- a/worlds/kdl3/src/kdl3_basepatch.asm +++ b/worlds/kdl3/src/kdl3_basepatch.asm @@ -240,6 +240,9 @@ MainLoopHook: BEQ .Return ; return if we are LDA $5541 ; gooey status BPL .Slowness ; gooey is already spawned + LDA $39D1 ; is kirby alive? + BEQ .Slowness ; branch if he isn't + ; maybe BMI here too? LDA $8080 CMP #$0000 ; did we get a gooey trap BEQ .Slowness ; branch if we did not @@ -432,17 +435,47 @@ AnimalFriendSpawn: BNE .Return XBA PHA + PHX + PHA + LDX #$0000 + .CheckSpawned: + LDA $05CA, X + BNE .Continue + LDA #$0002 + CMP $074A, X + BNE .ContinueCheck + PLA + PHA + XBA + CMP $07CA, X + BEQ .AlreadySpawned + .ContinueCheck: + INX + INX + BRA .CheckSpawned + .Continue: + PLA + PLX ASL TAY PLA INC CMP $8000, Y ; do we have this animal friend BEQ .Return ; we have this animal friend + .False: INX .Return: PLY LDA #$9999 RTL + .AlreadySpawned: + PLA + PLX + ASL + TAY + PLA + BRA .False + WriteBWRAM: LDY #$6001 ;starting addr diff --git a/worlds/kdl3/test/test_shuffles.py b/worlds/kdl3/test/test_shuffles.py index 852f35dc5c..ffa70977f7 100644 --- a/worlds/kdl3/test/test_shuffles.py +++ b/worlds/kdl3/test/test_shuffles.py @@ -1,6 +1,7 @@ from typing import List, Tuple, Optional from . import KDL3TestBase from ..room import KDL3Room +from ..names import animal_friend_spawns class TestCopyAbilityShuffle(KDL3TestBase): @@ -174,6 +175,14 @@ class TestAnimalShuffle(KDL3TestBase): self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}") + class TestAllShuffle(KDL3TestBase): options = { @@ -238,6 +247,14 @@ class TestAllShuffle(KDL3TestBase): self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}") + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities