diff --git a/BaseClasses.py b/BaseClasses.py index f686c3bc..dcf7ff69 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1686,13 +1686,13 @@ class Entrance(object): self.temp_path = [] def can_reach(self, state): - # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror - multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True), - 'Missing Smith': ('Frog', True, False, True, True), - 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True), - 'Dark Palace Button':('Kiki', True, False, False, False), - 'Old Man Drop Off': ('Lost Old Man', True, False, False, False), - 'Revealing Light': ('Suspicious Maiden', False, False, False, False) + # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror + multi_step_locations = { 'Pyramid Crack': ('Pick Up Big Bomb', True, True, False, True), + 'Missing Smith': ('Get Frog', True, False, True, True), + 'Middle Aged Man': ('Pick Up Purple Chest', True, False, True, True), + 'Dark Palace Button': ('Pick Up Kiki', True, False, False, False), + 'Old Man Drop Off': ('Escort Old Man', True, False, False, False), + 'Revealing Light': ('Maiden Rescued', False, False, False, False) } if self.name in multi_step_locations: @@ -1701,7 +1701,10 @@ class Entrance(object): multi_step_loc = multi_step_locations[self.name] if world.shuffle_followers[self.player]: multi_step_loc = (multi_step_loc[0], self.name == 'Pyramid Crack', multi_step_loc[2], True, True) - step_location = world.get_location(multi_step_loc[0], self.player) + step_location = world.find_items(multi_step_loc[0], self.player) + if len(step_location) == 0: + return False + step_location = step_location[0] if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_loc[1], multi_step_loc[2], multi_step_loc[3], multi_step_loc[4]) and self.access_rule(state): if not self in state.path: path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 277596e9..aa40a360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.6.1.8 +- Fixed follower placement and logic +- Fixed error with HC Courtyard Tree Pull + ## 0.6.1.7 - \~Merged in DR v1.5.2~ - Reverted key count update diff --git a/ItemList.py b/ItemList.py index 2a26d70a..65121389 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1695,16 +1695,17 @@ def set_event_item(world, player, location_name, item_name=None): def shuffle_event_items(world, player): if world.shuffle_followers[player]: + all_state = world.get_all_state(keys=True) available_quests = follower_quests.copy() available_pickups = [quests[0] for quests in available_quests.values()] + # finalize customizer followers first for loc_name in follower_quests.keys(): loc = world.get_location(loc_name, player) if loc.item: set_event_item(world, player, loc_name) available_quests.pop(loc_name) available_pickups.remove(loc.item.name) - if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups: zelda_dropoff = 'Zelda Pickup' @@ -1715,20 +1716,22 @@ def shuffle_event_items(world, player): available_pickups.remove(zelda_pickup) set_event_item(world, player, zelda_dropoff, zelda_pickup) - random.shuffle(available_pickups) + # randomize the follower pickups, but ensure that the last items are the unrestrictive ones + unrestrictive_pickups = [p for p in ['Zelda Herself', 'Sign Vandalized'] if p in available_pickups] + restrictive_pickups = [p for p in available_pickups if p not in unrestrictive_pickups] + random.shuffle(restrictive_pickups) + random.shuffle(unrestrictive_pickups) + available_pickups = restrictive_pickups + unrestrictive_pickups - restricted_pickups = { 'Get Frog': 'Dark Blacksmith Ruins'} - for pickup in restricted_pickups: - restricted_quests = [q for q in available_quests.keys() if q not in restricted_pickups[pickup]] - random.shuffle(restricted_quests) - quest = restricted_quests.pop() - available_quests.pop(quest) - available_pickups.remove(pickup) - set_event_item(world, player, quest, pickup) + pickup_items = ItemFactory(available_pickups, player) + follower_locations = [world.get_location(loc_name, player) for loc_name in available_quests.keys()] + random.shuffle(follower_locations) - for pickup in available_pickups: - quest, _ = available_quests.popitem() - set_event_item(world, player, quest, pickup) + fill_restrictive(world, all_state, follower_locations, pickup_items, single_player_placement=True) + for loc_name in available_quests.keys(): + loc = world.get_location(loc_name, player) + if loc.item: + set_event_item(world, player, loc_name) def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 616a85a2..02255592 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.6.1.7' +version_number = '0.6.1.8' # branch indicator is intentionally different across branches version_branch = '' diff --git a/Rules.py b/Rules.py index 4d06a4ce..88a8b4e9 100644 --- a/Rules.py +++ b/Rules.py @@ -1806,7 +1806,9 @@ def standard_rules(world, player): add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) if not world.is_premature_copied_world: - add_rule(world.get_location('Hyrule Castle Courtyard Tree Pull', player), lambda state: state.has('Zelda Delivered', player)) + loc = world.get_location_unsafe('Hyrule Castle Courtyard Tree Pull', player) + if loc: + add_rule(loc, lambda state: state.has('Zelda Delivered', player)) # don't allow bombs to get past here before zelda is rescued set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index 32909d37..90ee3fba 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -2116,8 +2116,13 @@ class EnemyTable: self.room_map = defaultdict(list) self.special_bitmasks = None - def write_sprite_data_to_rom(self, rom): - pointer_address = snes_to_pc(0x09D62E) + def write_sprite_data_to_rom(self, rom, pointer_table): + ow_data_end = pointer_table['ow_sprites'][0] + pointer_table['ow_sprites'][1] + if ow_data_end > pointer_table['uw_sprites'][2]: + # moving pointer table down + pointer_table['uw_sprites'][2] = ow_data_end + rom.write_bytes(snes_to_pc(pointer_table['uw_sprites'][3]), int16_as_bytes(ow_data_end & 0xFFFF)) + pointer_address = snes_to_pc(pointer_table['uw_sprites'][2]) data_pointer = snes_to_pc(0x288000) empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_bytes(data_pointer, [0x00, 0xff]) diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index f082a4f5..2ecfc518 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -29,6 +29,11 @@ class DataTables: self.ow_enemy_table = None self.pot_secret_table = None self.overworld_sprite_sheets = None + self.pointer_addresses = { + # table: [data_start, data_size, pointer_table, references] + 'ow_sprites': [ 0x09CB41, None, (0x09C881, 0x09C901, 0x09CA21), None ], + 'uw_sprites': [ 0x09D92E, None, 0x09D62E, 0x09C298 ], + } # associated data self.sprite_requirements = None @@ -77,13 +82,6 @@ class DataTables: # bank 0A uses 372A bytes # bank 1F uses 77CE bytes: total is about a bank and a half # probably should reuse bank 1F if writing all the rooms out - for sheet in self.sprite_sheets.values(): - sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3 - if self.uw_enemy_table.size() > 0x2800: - raise Exception('Sprite table is too big for current area') - self.uw_enemy_table.write_sprite_data_to_rom(rom) - self.uw_enemy_table.check_special_bitmasks_size() - self.uw_enemy_table.write_special_bitmask_table(rom) for area_id, sheet in self.overworld_sprite_sheets.items(): if area_id in [0x80, 0x81]: offset = area_id - 0x80 # 02E575 for special areas? @@ -95,7 +93,14 @@ class DataTables: # _00FAC1 is LW post-aga # _00FB01 is DW # _00FA41 is rain state + for sheet in self.sprite_sheets.values(): + sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3 self.write_ow_sprite_data_to_rom(rom) + if self.uw_enemy_table.size() > 0x2800: + raise Exception('Sprite table is too big for current area') + self.uw_enemy_table.write_sprite_data_to_rom(rom, self.pointer_addresses) + self.uw_enemy_table.check_special_bitmasks_size() + self.uw_enemy_table.write_special_bitmask_table(rom) for sprite, stats in self.enemy_stats.items(): # write health to rom if stats.health is not None: @@ -133,13 +138,13 @@ class DataTables: def write_ow_sprite_data_to_rom(self, rom): # calculate how big this table is going to be? - # bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 + bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 + self.pointer_addresses['ow_sprites'][1] = bytes # ending_byte = 0x09CB3B + bytes - max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard) + max_per_state = {0: 0x40, 1: 0x90, 2: 0x90} - pointer_address = snes_to_pc(0x09C881) - # currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard) - data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard) + pointer_address = snes_to_pc(self.pointer_addresses['ow_sprites'][2][0]) + data_pointer = snes_to_pc(self.pointer_addresses['ow_sprites'][0]) empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_byte(data_pointer, 0xff) cached_dark_world = {}