diff --git a/BaseClasses.py b/BaseClasses.py index aaaf8b69..9b9e15e9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -128,6 +128,7 @@ class World(object): set_player_attr('treasure_hunt_count', 0) set_player_attr('keydropshuffle', False) + set_player_attr('mixed_travel', 'prevent') def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' diff --git a/CLI.py b/CLI.py index 459002e7..5634c14b 100644 --- a/CLI.py +++ b/CLI.py @@ -95,7 +95,7 @@ def parse_cli(argv, no_defaults=False): 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', - 'remote_items', 'keydropshuffle']: + 'remote_items', 'keydropshuffle', 'mixed_travel']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -145,6 +145,7 @@ def parse_settings(): "intensity": 2, "experimental": False, "dungeon_counters": "default", + "mixed_travel": "prevent", "multi": 1, "names": "", diff --git a/DoorShuffle.py b/DoorShuffle.py index fe91731d..426e9501 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -154,20 +154,39 @@ def vanilla_key_logic(world, player): world.dungeon_layouts[player][builder.name] = builder add_inaccessible_doors(world, player) - for builder in builders: - origin_list = find_accessible_entrances(world, player, default_dungeon_entrances[builder.name]) - start_regions = convert_regions(origin_list, world, player) - doors = convert_key_doors(default_small_key_doors[builder.name], world, player) - key_layout = build_key_layout(builder, start_regions, doors, world, player) - valid = validate_key_layout(key_layout, world, player) - if not valid: - logging.getLogger('').warning('Vanilla key layout not valid %s', builder.name) - builder.key_door_proposal = doors - if player not in world.key_logic.keys(): - world.key_logic[player] = {} - analyze_dungeon(key_layout, world, player) - world.key_logic[player][builder.name] = key_layout.key_logic - log_key_logic(builder.name, key_layout.key_logic) + entrances_map, potentials, connections = determine_entrance_list(world, player) + + enabled_entrances = {} + sector_queue = deque(builders) + last_key, loops = None, 0 + while len(sector_queue) > 0: + builder = sector_queue.popleft() + + split_dungeon = builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods') + origin_list = list(entrances_map[builder.name]) + find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name) + if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player): + if last_key == builder.name or loops > 1000: + origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin' + raise Exception('Infinite loop detected for "%s" located at %s' % (builder.name, origin_name)) + sector_queue.append(builder) + last_key = builder.name + loops += 1 + else: + find_new_entrances(builder.master_sector, entrances_map, connections, potentials, enabled_entrances, world, player) + start_regions = convert_regions(origin_list, world, player) + doors = convert_key_doors(default_small_key_doors[builder.name], world, player) + key_layout = build_key_layout(builder, start_regions, doors, world, player) + valid = validate_key_layout(key_layout, world, player) + if not valid: + logging.getLogger('').warning('Vanilla key layout not valid %s', builder.name) + builder.key_door_proposal = doors + if player not in world.key_logic.keys(): + world.key_logic[player] = {} + analyze_dungeon(key_layout, world, player) + world.key_logic[player][builder.name] = key_layout.key_logic + log_key_logic(builder.name, key_layout.key_logic) + last_key = None if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: validate_vanilla_key_logic(world, player) @@ -837,6 +856,7 @@ def cross_dungeon(world, player): for key in dungeon_regions.keys(): all_regions += dungeon_regions[key] all_sectors.extend(convert_to_sectors(all_regions, world, player)) + merge_sectors(all_sectors, world, player) entrances, splits = create_dungeon_entrances(world, player) dungeon_builders = create_dungeon_builders(all_sectors, connections_tuple, world, player, entrances, splits) for builder in dungeon_builders.values(): @@ -1076,6 +1096,30 @@ def convert_to_sectors(region_names, world, player): return sectors +def merge_sectors(all_sectors, world, player): + if world.mixed_travel[player] == 'force': + sectors_to_remove = {} + merge_sectors = {} + for sector in all_sectors: + r_set = sector.region_set() + if 'PoD Arena Ledge' in r_set: + sectors_to_remove['Arenahover'] = sector + elif 'PoD Big Chest Balcony' in r_set: + sectors_to_remove['Hammerjump'] = sector + elif 'Mire Chest View' in r_set: + sectors_to_remove['Mire BJ'] = sector + elif 'PoD Falling Bridge Ledge' in r_set: + merge_sectors['Hammerjump'] = sector + elif 'PoD Arena Bridge' in r_set: + merge_sectors['Arenahover'] = sector + elif 'Mire BK Chest Ledge' in r_set: + merge_sectors['Mire BJ'] = sector + for key, old_sector in sectors_to_remove.items(): + merge_sectors[key].regions.extend(old_sector.regions) + merge_sectors[key].outstanding_doors.extend(old_sector.outstanding_doors) + all_sectors.remove(old_sector) + + # those with split region starts like Desert/Skull combine for key layouts def combine_layouts(recombinant_builders, dungeon_builders, entrances_map): for recombine in recombinant_builders.values(): @@ -1697,6 +1741,7 @@ class DROptions(Flag): Town_Portal = 0x02 # If on, Players will start with mirror scroll Map_Info = 0x04 Debug = 0x08 + Rails = 0x10 # If on, draws rails Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required diff --git a/Main.py b/Main.py index 3ec96c9d..e8c45470 100644 --- a/Main.py +++ b/Main.py @@ -24,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names -__version__ = '0.2.0.4-u' +__version__ = '0.2.0.5-u' class EnemizerError(RuntimeError): pass @@ -67,6 +67,7 @@ def main(args, seed=None, fish=None): world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish world.keydropshuffle = args.keydropshuffle.copy() + world.mixed_travel = args.mixed_travel.copy() world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} @@ -374,6 +375,7 @@ def copy_world(world): ret.intensity = world.intensity.copy() ret.experimental = world.experimental.copy() ret.keydropshuffle = world.keydropshuffle.copy() + ret.mixed_travel = world.mixed_travel.copy() for player in range(1, world.players + 1): if world.mode[player] != 'inverted': diff --git a/README.md b/README.md index 7ed5b80f..1b890e7e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Alternatively, run ```Gui.py``` for a simple graphical user interface. (WIP) Only extra settings are found here. All entrance randomizer settings are supported. See their [readme](https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/master/README.md) -## Door Shuffle +## Door Shuffle (--doorShuffle) ### Basic @@ -36,7 +36,7 @@ Doors are shuffled between dungeons as well. Doors are not shuffled. -## Intensity +## Intensity (--intensity number) #### Level 1 Normal door and spiral staircases are shuffled @@ -45,14 +45,32 @@ Same as Level 1 plus open edges and straight staircases are shuffled. #### Level 3 Same as Level 2 plus Dungeon Lobbies are shuffled -## KeyDropShuffle - ---keydropshuffle in CLI +## KeyDropShuffle (--keydropshuffle) Adds 33 new locations to the randomization pool. The 32 small keys found under pots and dropped by enemies and the Big Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds 32 generic keys to the pool instead. +## Crossed Dungeon Specific Settings + +### Mixed Travel (--mixed_travel value) + +Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump two sections of a supertile that are +otherwise unconnected logically can be reach using these glitches. To prevent the player from unintentionally + +#### Prevent + +Rails are added the 3 spots to prevent this tricks. This setting is recommend for those learning crossed dungeon mode to +learn what is dangerous and what is not. No logic seeds ignore this setting. + +#### Allow + +The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not. + +#### Force + +The two disjointed sections are forced to be in the same dungeon but never logically required to complete that game. + ## Map/Compass/Small Key/Big Key shuffle (aka Keysanity) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b2b0336..d75fef8a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,13 @@ and where enemies drop keys. This includes 32 small key location and the ball an Big Key. * Multiworld untested - May need changes to MultiClient/MultiServer to recognize new locations * GT Big Key count / total location count needs to be updated +* --mixed_travel setting added + * Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump two sections of a supertile that are +otherwise unconnected logically can be reach using these glitches. To prevent the player from unintentionally + * prevent: Rails are added the 3 spots to prevent this tricks. This setting is recommend for those learning + crossed dungeon mode to learn what is dangerous and what is not. No logic seeds ignore this setting. + * allow: The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not. + * force: The two disjointed sections are forced to be in the same dungeon but never logically required to complete that game. ### Experimental features @@ -27,11 +34,10 @@ Big Key. * Fixed a problem ER shuffle generation that did not account for lobbies moving around * Fixed a problem with camera unlock (GT Mimics and Mire Minibridge) * Fixed a problem with bad-pseudo layer at PoD map Balcony (unable to hit switch with Bomb) +* Fixed a problem with the Ganon hint when hints are turned off # Known Issues (I'm planning to fix theese in this Unstable iteration hopefully) -* Hammerjump (et al) rails -* Backward TR Crystal Maze locking Somaria -* Ganon hint when hints are turned off not correct \ No newline at end of file +* Backward TR Crystal Maze locking Somaria \ No newline at end of file diff --git a/Rom.py b/Rom.py index e0758e4a..4e7e641f 100644 --- a/Rom.py +++ b/Rom.py @@ -24,7 +24,7 @@ from EntranceShuffle import door_addresses, exit_ids JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'c83a85f4dca8a0f264280aecde8e41c2' +RANDOMIZERBASEHASH = '6904a4588b21d1674e1fa25e96da8339' class JsonRom(object): @@ -624,6 +624,10 @@ def patch_rom(world, rom, player, team, enemized): if world.experimental[player]: dr_flags |= DROptions.Map_Info dr_flags |= DROptions.Debug + if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ + and world.mixed_travel[player] == 'prevent': + dr_flags |= DROptions.Rails + # fix hc big key problems if world.doorShuffle[player] == 'crossed' or world.keydropshuffle[player]: @@ -1358,7 +1362,7 @@ def patch_rom(world, rom, player, team, enemized): item_dungeon = hera_basement.item.name.split('(')[1][:-1] if item_dungeon == 'Escape': item_dungeon = 'Hyrule Castle' - is_small_key_this_dungeon = hera_basement.dungeon.name == item_dungeon + is_small_key_this_dungeon = hera_basement.parent_region.dungeon.name == item_dungeon if is_small_key_this_dungeon: rom.write_byte(0x4E3BB, 0xE4) else: diff --git a/Utils.py b/Utils.py index 6ca98e5b..18d46667 100644 --- a/Utils.py +++ b/Utils.py @@ -575,7 +575,8 @@ def extract_data_from_jp_rom(rom): with open(rom, 'rb') as stream: rom_data = bytearray(stream.read()) - rooms = [0x7b, 0x7c, 0x7d, 0x8b, 0x8c, 0x8d, 0x9b, 0x9c, 0x9d] + # rooms = [0x7b, 0x7c, 0x7d, 0x8b, 0x8c, 0x8d, 0x9b, 0x9c, 0x9d] + rooms = [0x1a, 0x2a, 0xd1] for room in rooms: b2idx = room*2 b3idx = room*3 @@ -652,6 +653,6 @@ if __name__ == '__main__': # make_new_base2current() # read_entrance_data(old_rom=sys.argv[1]) # room_palette_data(old_rom=sys.argv[1]) - extract_data_from_us_rom(sys.argv[1]) - # extract_data_from_jp_rom(sys.argv[1]) + # extract_data_from_us_rom(sys.argv[1]) + extract_data_from_jp_rom(sys.argv[1]) diff --git a/data/base2current.bps b/data/base2current.bps index 0f7c688a..04a89541 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 25aa6ce5..04400bbe 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -63,6 +63,13 @@ "action": "store_true", "type": "bool" }, + "mixed_travel" : { + "choices": [ + "prevent", + "allow", + "force" + ] + }, "timer": { "choices": [ "none", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 2c9d2081..d60fa7c9 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -245,6 +245,12 @@ "keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "keydropshuffle": [ "Key Drops (Pots and Enemies) are shuffled and other items can take their place (default: %(default)s)" ], + "mixed_travel": [ + "How to handle potential traversal between dungeon in Crossed door shuffle", + "Prevent: Rails are placed to prevent bombs jump and hovering from changing dungeon except with glitched logic settings", + "Allow: Take the rails off, \"I know what I'm doing\"", + "Force: Force these troublesome connections to be in the same dungeon (but not in logic). No rails will appear" + ], "retro": [ "Keys are universal, shooting arrows costs rupees,", "and a few other little things make this more like Zelda-1. (default: %(default)s)" diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 5417eaf3..852491df 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -72,6 +72,10 @@ "randomizer.dungeon.dungeon_counters.on": "On", "randomizer.dungeon.dungeon_counters.pickup": "On Compass Pickup", + "randomizer.dungeon.mixed_travel": "Mixed Dungeon Travel ", + "randomizer.dungeon.mixed_travel.prevent": "Prevent Mixed Dungeon Travel", + "randomizer.dungeon.mixed_travel.allow": "Allow Mixed Dungeon Travel", + "randomizer.dungeon.mixed_travel.force": "Force Reachable Areas to Same Dungeon", "randomizer.enemizer.potshuffle": "Pot Shuffle", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 736f2aeb..f6c52bff 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -19,7 +19,7 @@ "random" ], "config": { - "width": 40 + "width": 35 } }, "experimental": { "type": "checkbox" }, @@ -32,6 +32,18 @@ "on", "pickup" ] + }, + "mixed_travel": { + "type" : "selectbox", + "default": "auto", + "options": [ + "prevent", + "allow", + "force" + ], + "config": { + "width": 35 + } } } } diff --git a/source/classes/constants.py b/source/classes/constants.py index 2d1720b5..0d2157cb 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -90,7 +90,8 @@ SETTINGSTOPROCESS = { "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", "experimental": "experimental", - "dungeon_counters": "dungeon_counters" + "dungeon_counters": "dungeon_counters", + "mixed_travel": "mixed_travel" }, "gameoptions": { "hints": "hints",