From b21564d5aa738c0174a070434a71b7dcb1299fc2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 29 Jun 2021 16:34:28 -0600 Subject: [PATCH 001/104] Combinatoric approach revised (KLA1) Backported some fixes --- BaseClasses.py | 359 +++++++++++++++++++++++++++++++++++++++------- DoorShuffle.py | 35 +---- Dungeons.py | 15 -- Fill.py | 16 ++- KeyDoorShuffle.py | 146 +++++++++++++------ Main.py | 1 + Rules.py | 52 +++++-- Utils.py | 24 ++++ 8 files changed, 490 insertions(+), 158 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ba685ac2..54a45817 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -458,17 +458,26 @@ class World(object): class CollectionState(object): - def __init__(self, parent): - self.prog_items = Counter() + def __init__(self, parent, skip_init=False): self.world = parent - self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} - self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} - self.events = [] - self.path = {} - self.locations_checked = set() - self.stale = {player: True for player in range(1, parent.players + 1)} - for item in parent.precollected_items: - self.collect(item, True) + if not skip_init: + self.prog_items = Counter() + self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} + self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} + self.events = [] + self.path = {} + self.locations_checked = set() + self.stale = {player: True for player in range(1, parent.players + 1)} + for item in parent.precollected_items: + self.collect(item, True) + # reached vs. opened in the counter + self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)} + self.reached_doors = {player: set() for player in range(1, parent.players + 1)} + self.opened_doors = {player: set() for player in range(1, parent.players + 1)} + self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)} + + self.ghost_keys = Counter() + self.dungeon_limits = None def update_reachable_regions(self, player): self.stale[player] = False @@ -479,66 +488,261 @@ class CollectionState(object): start = self.world.get_region('Menu', player) if not start in rrp: rrp[start] = CrystalBarrier.Orange - for exit in start.exits: - bc[exit] = CrystalBarrier.Orange + for conn in start.exits: + bc[conn] = CrystalBarrier.Orange queue = deque(self.blocked_connections[player].items()) + self.traverse_world(queue, rrp, bc, player) + unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations + if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) + and x not in self.locations_checked and x.can_reach(self)] + if len(unresolved_events) == 0: + self.check_key_doors_in_dungeons(rrp, player) + + def traverse_world(self, queue, rrp, bc, player): # run BFS on all connections, and keep track of those blocked by missing items - while True: - try: - connection, crystal_state = queue.popleft() - new_region = connection.connected_region - if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state): - bc.pop(connection, None) - elif connection.can_reach(self): - if new_region.type == RegionType.Dungeon: - new_crystal_state = crystal_state - for exit in new_region.exits: - door = exit.door - if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self): - new_crystal_state = CrystalBarrier.Either - break - if new_region in rrp: - new_crystal_state |= rrp[new_region] + while len(queue) > 0: + connection, crystal_state = queue.popleft() + new_region = connection.connected_region + if not self.should_visit(new_region, rrp, crystal_state, player): + bc.pop(connection, None) + elif connection.can_reach(self): + bc.pop(connection, None) + if new_region.type == RegionType.Dungeon: + new_crystal_state = crystal_state + if new_region in rrp: + new_crystal_state |= rrp[new_region] - rrp[new_region] = new_crystal_state - - for exit in new_region.exits: - door = exit.door - if door is not None and not door.blocked: + rrp[new_region] = new_crystal_state + for conn in new_region.exits: + door = conn.door + if door is not None and not door.blocked: + if self.valid_crystal(door, new_crystal_state): door_crystal_state = door.crystal if door.crystal else new_crystal_state - bc[exit] = door_crystal_state - queue.append((exit, door_crystal_state)) - elif door is None: - queue.append((exit, new_crystal_state)) + bc[conn] = door_crystal_state + queue.append((conn, door_crystal_state)) + elif door is None: + # note: no door in dungeon indicates what exactly? (always traversable)? + queue.append((conn, new_crystal_state)) + else: + new_crystal_state = CrystalBarrier.Orange + rrp[new_region] = new_crystal_state + for conn in new_region.exits: + bc[conn] = new_crystal_state + queue.append((conn, new_crystal_state)) + + self.path[new_region] = (new_region.name, self.path.get(connection, None)) + + # Retry connections if the new region can unblock them + if new_region.name in indirect_connections: + new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) + if new_entrance in bc and new_entrance.parent_region in rrp: + new_crystal_state = rrp[new_entrance.parent_region] + if (new_entrance, new_crystal_state) not in queue: + queue.append((new_entrance, new_crystal_state)) + # else those connections that are not accessible yet + if self.is_small_door(connection) and not self.world.retro[player]: # todo: retro + door = connection.door + dungeon_name = connection.parent_region.dungeon.name # todo: universal + key_logic = self.world.key_logic[player][dungeon_name] + if door.name not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door.name) + if key_logic.sm_doors[door]: + self.reached_doors[player].add(key_logic.sm_doors[door].name) + if not connection.can_reach(self): + checklist = self.dungeons_to_check[player][dungeon_name] + checklist[connection.name] = (connection, crystal_state) + elif door.name not in self.opened_doors[player]: + opened_doors = self.opened_doors[player] + door = connection.door + if door.name not in opened_doors: + self.door_counter[player][1][dungeon_name] += 1 + opened_doors.add(door.name) + key_logic = self.world.key_logic[player][dungeon_name] + if key_logic.sm_doors[door]: + opened_doors.add(key_logic.sm_doors[door].name) + + def should_visit(self, new_region, rrp, crystal_state, player): + if not new_region: + return False + if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player): + return False + if new_region not in rrp: + return True + if new_region.type != RegionType.Dungeon: + return False + return (rrp[new_region] & crystal_state) != crystal_state + + def possibly_connected_to_dungeon(self, new_region, player): + if new_region.dungeon: + return new_region.dungeon.name in self.dungeon_limits + else: + return new_region.name in self.world.inaccessible_regions[player] + + @staticmethod + def valid_crystal(door, new_crystal_state): + return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either + or new_crystal_state == door.crystal) + + def check_key_doors_in_dungeons(self, rrp, player): + for dungeon_name, checklist in self.dungeons_to_check[player].items(): + init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) + key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal + remaining_keys = key_total - self.door_counter[player][1][dungeon_name] + if not init_door_candidates or remaining_keys == 0: + continue + dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()} + + def valid_d_door(x): + return x in dungeon_doors + + child_states = deque() + child_states.append(self) + visited_opened_doors = set() + visited_opened_doors.add(frozenset(self.opened_doors[player])) + terminal_states, done, common_regions, common_bc, common_doors = [], False, {}, {}, set() + while not done: + terminal_states.clear() + while len(child_states) > 0: + next_child = child_states.popleft() + door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) + if door_candidates: + for chosen_door in door_candidates: + child_state = next_child.copy() + child_queue = deque() + child_state.door_counter[player][1][dungeon_name] += 1 + if isinstance(chosen_door, tuple): + child_state.opened_doors[player].add(chosen_door[0]) + child_state.opened_doors[player].add(chosen_door[1]) + if chosen_door[0] in checklist: + child_queue.append(checklist[chosen_door[0]]) + if chosen_door[1] in checklist: + child_queue.append(checklist[chosen_door[1]]) + else: + child_state.opened_doors[player].add(chosen_door) + if chosen_door in checklist: + child_queue.append(checklist[chosen_door]) + if child_state.opened_doors[player] not in visited_opened_doors: + done = False + while not done: + rrp_ = child_state.reachable_regions[player] + bc_ = child_state.blocked_connections[player] + self.dungeon_limits = [dungeon_name] + child_state.traverse_world(child_queue, rrp_, bc_, player) + new_events = child_state.sweep_for_events_once() + child_state.stale[player] = False + if new_events: + for conn in bc_: + if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: + child_queue.append((conn, bc_[conn])) + done = not new_events + visited_opened_doors.add(frozenset(child_state.opened_doors[player])) + child_states.append(child_state) else: - new_crystal_state = CrystalBarrier.Orange - rrp[new_region] = new_crystal_state - bc.pop(connection, None) - for exit in new_region.exits: - bc[exit] = new_crystal_state - queue.append((exit, new_crystal_state)) + terminal_states.append(next_child) + common_regions, common_doors, first = {}, set(), True + for term_state in terminal_states: + t_rrp = term_state.reachable_regions[player] + if first: + first = False + common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + else: + cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() + if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} + common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + done = len(child_states) == 0 - self.path[new_region] = (new_region.name, self.path.get(connection, None)) + terminal_queue = deque() + for door in common_doors: + self.opened_doors[player].add(door) + if door in checklist: + terminal_queue.append(checklist[door]) + if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + self.door_counter[player][1][dungeon_name] += 1 - # Retry connections if the new region can unblock them - if new_region.name in indirect_connections: - new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player) - if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp: - queue.append((new_entrance, rrp[new_entrance.parent_region])) - except IndexError: - break + self.dungeon_limits = [dungeon_name] + rrp_ = self.reachable_regions[player] + bc_ = self.blocked_connections[player] + self.traverse_world(terminal_queue, rrp_, bc_, player) + self.dungeon_limits = None + rrp = self.reachable_regions[player] + missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} + for k in missing_regions: + rrp[k] = missing_regions[k] + checklist.clear() + + @staticmethod + def comb_crys(a, b): + return a if a == b or a != CrystalBarrier.Either else b + + @staticmethod + def crys_agree(a, b): + return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either + + def find_door_pair(self, player, dungeon_name, name): + for door in self.world.key_logic[player][dungeon_name].sm_doors.keys(): + if door.name == name: + paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door] + return paired_door.name if paired_door else None + return None + + @staticmethod + def should_explore_child_state(state, dungeon_name, player): + small_key_name = dungeon_keys[dungeon_name] # todo: universal + key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(small_key_name, player)] + remaining_keys = key_total - state.door_counter[player][1][dungeon_name] + unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name] + if remaining_keys > 0 and unopened_doors > 0: + key_logic = state.world.key_logic[player][dungeon_name] # todo: universal + door_candidates, skip = [], set() + for door, paired in key_logic.sm_doors.items(): + if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]: + if door.name not in skip: + if paired: + door_candidates.append((door.name, paired.name)) + skip.add(paired.name) + else: + door_candidates.append(door.name) + return door_candidates + return None + + @staticmethod + def print_rrp(rrp): + logger = logging.getLogger('') + logger.debug('RRP Checking') + for region, packet in rrp.items(): + new_crystal_state, logic, path = packet + logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})') + for i in range(0, len(logic)): + logger.debug(f'{logic[i]}') + logger.debug(f'{",".join(str(x) for x in path[i])}') def copy(self): - ret = CollectionState(self.world) + ret = CollectionState(self.world, skip_init=True) ret.prog_items = self.prog_items.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) ret.path = copy.copy(self.path) ret.locations_checked = copy.copy(self.locations_checked) + ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)} + ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1])) + for player in range(1, self.world.players + 1)} + ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)} + ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)} + # todo: verify if this isn't copied deep enough + ret.dungeons_to_check = { + player: defaultdict(dict, {name: copy.copy(checklist) + for name, checklist in self.dungeons_to_check[player].items()}) + for player in range(1, self.world.players + 1)} + ret.ghost_keys = self.ghost_keys.copy() return ret def can_reach(self, spot, resolution_hint=None, player=None): @@ -556,6 +760,19 @@ class CollectionState(object): return spot.can_reach(self) + def sweep_for_events_once(self, key_only=False, locations=None): + if locations is None: + locations = self.world.get_filled_locations() + checked_locations = set([l for l in locations if l in self.locations_checked]) + reachable_events = [location for location in locations if location.event and + (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) + and location.can_reach(self)] + reachable_events = self._do_not_flood_the_keys(reachable_events) + for event in reachable_events: + if event not in checked_locations: + self.events.append((event.name, event.player)) + self.collect(event.item, True, event) + return len(reachable_events) > len(checked_locations) def sweep_for_events(self, key_only=False, locations=None): # this may need improvement @@ -603,6 +820,13 @@ class CollectionState(object): or not self.location_can_be_flooded(flood_location)) return True + @staticmethod + def is_small_door(connection): + return connection and connection.door and connection.door.smallKey + + def is_door_open(self, door_name, player): + return door_name in self.opened_doors[player] + @staticmethod def location_can_be_flooded(location): return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove'] @@ -1806,6 +2030,15 @@ class Item(object): def compass(self): return self.type == 'Compass' + @property + def dungeon(self): + if not self.smallkey and not self.bigkey and not self.map and not self.compass: + return None + item_dungeon = self.name.split('(')[1][:-1] + if item_dungeon == 'Escape': + item_dungeon = 'Hyrule Castle' + return item_dungeon + def __str__(self): return str(self.__unicode__()) @@ -2196,6 +2429,21 @@ dungeon_names = [ 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower' ] +dungeon_keys = { + 'Hyrule Castle': 'Small Key (Escape)', + 'Eastern Palace': 'Small Key (Eastern Palace)', + 'Desert Palace': 'Small Key (Desert Palace)', + 'Tower of Hera': 'Small Key (Tower of Hera)', + 'Agahnims Tower': 'Small Key (Agahnims Tower)', + 'Palace of Darkness': 'Small Key (Palace of Darkness)', + 'Swamp Palace': 'Small Key (Swamp Palace)', + 'Skull Woods': 'Small Key (Skull Woods)', + 'Thieves Town': 'Small Key (Thieves Town)', + 'Ice Palace': 'Small Key (Ice Palace)', + 'Misery Mire': 'Small Key (Misery Mire)', + 'Turtle Rock': 'Small Key (Turtle Rock)', + 'Ganons Tower': 'Small Key (Ganons Tower)' +} class PotItem(FastEnum): Nothing = 0x0 @@ -2346,3 +2594,10 @@ class Settings(object): args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] args.shufflepots[p] = True if settings[7] & 0x4 else False + + +@unique +class KeyRuleType(Enum): + WorstCase = 0 + AllowSmall = 1 + Lock = 2 diff --git a/DoorShuffle.py b/DoorShuffle.py index ee38f00c..53676cb9 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1,22 +1,21 @@ import random from collections import defaultdict, deque import logging -import operator as op import time from enum import unique, Flag from typing import DefaultDict, Dict, List -from functools import reduce -from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo +from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from Doors import reset_portals from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts -from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints +from Dungeons import dungeon_bigs, dungeon_hints from Items import ItemFactory from RoomData import DoorKind, PairedDoor, reset_rooms from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException -from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout +from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout +from Utils import ncr, kth_combination def link_doors(world, player): @@ -212,8 +211,8 @@ def vanilla_key_logic(world, 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) - 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) + # 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) # some useful functions @@ -1576,28 +1575,6 @@ def find_key_door_candidates(region, checked, world, player): return candidates, checked_doors -def kth_combination(k, l, r): - if r == 0: - return [] - elif len(l) == r: - return l - else: - i = ncr(len(l)-1, r-1) - if k < i: - return l[0:1] + kth_combination(k, l[1:], r-1) - else: - return kth_combination(k-i, l[1:], r) - - -def ncr(n, r): - if r == 0: - return 1 - r = min(r, n-r) - numerator = reduce(op.mul, range(n, n-r, -1), 1) - denominator = reduce(op.mul, range(1, r+1), 1) - return numerator / denominator - - def reassign_key_doors(builder, world, player): logger = logging.getLogger('') logger.debug('Key doors for %s', builder.name) diff --git a/Dungeons.py b/Dungeons.py index d5297400..6f0f2197 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,21 +375,6 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } -dungeon_keys = { - 'Hyrule Castle': 'Small Key (Escape)', - 'Eastern Palace': 'Small Key (Eastern Palace)', - 'Desert Palace': 'Small Key (Desert Palace)', - 'Tower of Hera': 'Small Key (Tower of Hera)', - 'Agahnims Tower': 'Small Key (Agahnims Tower)', - 'Palace of Darkness': 'Small Key (Palace of Darkness)', - 'Swamp Palace': 'Small Key (Swamp Palace)', - 'Skull Woods': 'Small Key (Skull Woods)', - 'Thieves Town': 'Small Key (Thieves Town)', - 'Ice Palace': 'Small Key (Ice Palace)', - 'Misery Mire': 'Small Key (Misery Mire)', - 'Turtle Rock': 'Small Key (Turtle Rock)', - 'Ganons Tower': 'Small Key (Ganons Tower)' -} dungeon_bigs = { 'Hyrule Castle': 'Big Key (Escape)', diff --git a/Fill.py b/Fill.py index c1488113..9c914726 100644 --- a/Fill.py +++ b/Fill.py @@ -199,6 +199,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None + valid_locations = [] for location in locations: if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place @@ -209,11 +210,16 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = if (not single_player_placement or location.player == item_to_place.player)\ and location.can_fill(test_state, item_to_place, perform_access_check)\ and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location - break - elif item_to_place.smallkey or item_to_place.bigkey: + # todo: optimization: break instead of cataloging all valid locations + if not spot_to_fill: + spot_to_fill = location + valid_locations.append(location) + + if item_to_place.smallkey or item_to_place.bigkey: location.item = None + logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations') + if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) @@ -250,9 +256,7 @@ def valid_key_placement(item, location, itempool, world): def track_outside_keys(item, location, world): if not item.smallkey: return - item_dungeon = item.name.split('(')[1][:-1] - if item_dungeon == 'Escape': - item_dungeon = 'Hyrule Castle' + item_dungeon = item.dungeon if location.player == item.player: loc_dungeon = location.parent_region.dungeon if loc_dungeon and loc_dungeon.name == item_dungeon: diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 549a739c..857ee479 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2,9 +2,9 @@ import itertools import logging from collections import defaultdict, deque -from BaseClasses import DoorType +from BaseClasses import DoorType, dungeon_keys, KeyRuleType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs +from Dungeons import dungeon_bigs from DungeonGenerator import ExplorationState, special_big_key_doors @@ -25,6 +25,7 @@ class KeyLayout(object): self.all_locations = set() self.item_locations = set() + self.found_doors = set() # bk special? # bk required? True if big chests or big doors exists @@ -54,6 +55,7 @@ class KeyLogic(object): self.location_rules = {} self.outside_keys = 0 self.dungeon = dungeon_name + self.sm_doors = {} def check_placement(self, unplaced_keys, big_key_loc=None): for rule in self.placement_rules: @@ -65,6 +67,15 @@ class KeyLogic(object): return False return True + def reset(self): + self.door_rules.clear() + self.bk_restricted.clear() + self.bk_locked.clear() + self.sm_restricted.clear() + self.bk_doors.clear() + self.bk_chests.clear() + self.placement_rules.clear() + class DoorRules(object): @@ -79,6 +90,8 @@ class DoorRules(object): self.small_location = None self.opposite = None + self.new_rules = {} # keyed by type, or type+lock_item -> number + class LocationRule(object): def __init__(self): @@ -209,8 +222,19 @@ def calc_max_chests(builder, key_layout, world, player): def analyze_dungeon(key_layout, world, player): + key_layout.key_logic.reset() key_layout.key_counters = create_key_counters(key_layout, world, player) key_logic = key_layout.key_logic + for door in key_layout.proposal: + if isinstance(door, tuple): + key_logic.sm_doors[door[0]] = door[1] + key_logic.sm_doors[door[1]] = door[0] + else: + if door.dest and door.type != DoorType.SpiralStairs: + key_logic.sm_doors[door] = door.dest + key_logic.sm_doors[door.dest] = door + else: + key_logic.sm_doors[door] = None find_bk_locked_sections(key_layout, world, player) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) @@ -247,8 +271,9 @@ def analyze_dungeon(key_layout, world, player): while len(child_queue) > 0: child, odd_counter, empty_flag = child_queue.popleft() if not child.bigKey and child not in doors_completed: - best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) - rule = create_rule(best_counter, key_counter, key_layout, world, player) + best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag) + rule = create_rule(best_counter, key_counter, world, player) + create_worst_case_rule(rule, best_counter, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) key_logic.door_rules[child.name] = rule @@ -258,7 +283,8 @@ def analyze_dungeon(key_layout, world, player): if ctr_id not in visited_cid: queue.append((child, next_counter)) visited_cid.add(ctr_id) - check_rules(original_key_counter, key_layout, world, player) + # todo: why is this commented out? + # check_rules(original_key_counter, key_layout, world, player) # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location for rule in key_logic.door_rules.values(): @@ -294,7 +320,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): else: placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) rule.check_locations_w_bk = accessible_loc - check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) + # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) else: if big_key_progress(key_counter) and only_sm_doors(key_counter): create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player) @@ -320,6 +346,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe rule.needed_keys_w_bk -= 1 +# this rule is suspect - commented out usages for now def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked): if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations): key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations)) @@ -478,7 +505,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a else: placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) rule.check_locations_w_bk = accessible_loc - check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) + # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) key_logic.placement_rules.append(rule) adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) @@ -538,6 +565,8 @@ def relative_empty_counter(odd_counter, key_counter): return False if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0: return False + if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0: + return False # important only if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0: return False @@ -594,33 +623,50 @@ def unique_child_door_2(child, key_counter): return True -def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? - ignored_doors = {door, door.dest} if door is not None else {} - finished = False - opened_doors = dict(key_counter.open_doors) - bk_opened = key_counter.big_key_opened - # new_counter = key_counter - last_counter = key_counter - while not finished: - door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) - if door_set is None or len(door_set) == 0: - finished = True - continue - for new_door in door_set: - proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} - bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) - bk_open = new_counter.big_key_opened - # this means the new_door invalidates the door / leads to the same stuff - if not empty_flag and relative_empty_counter(odd_counter, new_counter): - ignored_doors.add(new_door) - elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): - last_counter = new_counter - opened_doors = proposed_doors - bk_opened = bk_open - else: - ignored_doors.add(new_door) - return last_counter +# def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible? +# ignored_doors = {door, door.dest} if door is not None else {} +# finished = False +# opened_doors = dict(key_counter.open_doors) +# bk_opened = key_counter.big_key_opened +# # new_counter = key_counter +# last_counter = key_counter +# while not finished: +# door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk) +# if door_set is None or len(door_set) == 0: +# finished = True +# continue +# for new_door in door_set: +# proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} +# bk_open = bk_opened or new_door.bigKey +# new_counter = find_counter(proposed_doors, bk_open, key_layout) +# bk_open = new_counter.big_key_opened +# # this means the new_door invalidates the door / leads to the same stuff +# if not empty_flag and relative_empty_counter(odd_counter, new_counter): +# ignored_doors.add(new_door) +# elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player): +# last_counter = new_counter +# opened_doors = proposed_doors +# bk_opened = bk_open +# else: +# ignored_doors.add(new_door) +# return last_counter + + +def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag): + best, best_ctr, locations = 0, None, 0 + for code, counter in key_layout.key_counters.items(): + if door not in counter.open_doors: + if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations): + if not skip_bk or not counter.big_key_opened: + if empty_flag or not relative_empty_counter(odd_counter, counter): + best = counter.used_keys + best_ctr = counter + locations = count_locations(counter) + return best_ctr + + +def count_locations(ctr): + return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations) def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible? @@ -717,7 +763,7 @@ def calc_avail_keys(key_counter, world, player): return raw_avail - key_counter.used_keys -def create_rule(key_counter, prev_counter, key_layout, world, player): +def create_rule(key_counter, prev_counter, world, player): # prev_chest_keys = available_chest_small_keys(prev_counter, world) # prev_avail = prev_chest_keys + len(prev_counter.key_only_locations) chest_keys = available_chest_small_keys(key_counter, world, player) @@ -736,6 +782,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player): return DoorRules(rule_num, is_valid) +def create_worst_case_rule(rules, key_counter, world, player): + required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys + rules.new_rules[KeyRuleType.WorstCase] = required_keys + + def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player): if world.accessibility[player] != 'locations': counter = find_inverted_counter(door, parent_counter, key_layout, world, player) @@ -845,16 +896,16 @@ def big_key_drop_available(key_counter): def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player): if key_counter.big_key_opened: return - best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag) - bk_rule = create_rule(best_counter, key_counter, key_layout, world, player) + best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag) + bk_rule = create_rule(best_counter, key_counter, world, player) if bk_rule.small_key_num >= rule.small_key_num: return door_open = find_next_counter(door, best_counter, key_layout) ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors) dest_ignored = [] - for door in ignored_doors.keys(): - if door.dest not in ignored_doors: - dest_ignored.append(door.dest) + for d in ignored_doors.keys(): + if d.dest not in ignored_doors: + dest_ignored.append(d.dest) ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)} post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys()) unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations) @@ -862,8 +913,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la if len(unique_loc) > 0: # and bk_rule.is_valid rule.alternate_small_key = bk_rule.small_key_num rule.alternate_big_key_loc.update(unique_loc) - # elif not bk_rule.is_valid: - # key_layout.key_logic.bk_restricted.update(unique_loc) + if not door.bigKey: + rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1 def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout): @@ -935,6 +986,7 @@ def only_sm_doors(key_counter): return False return True + # doesn't count dest doors def count_unique_small_doors(key_counter, proposal): cnt = 0 @@ -1197,7 +1249,7 @@ def check_rules_deep(original_counter, key_layout, world, player): elif not door.bigKey: can_open = True if can_open: - can_progress = smalls_opened or not big_maybe_not_found + can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened next_counter = find_next_counter(door, counter, key_layout) c_id = cid(next_counter, key_layout) if c_id not in completed: @@ -1381,6 +1433,7 @@ def cnt_avail_big_locations(ttl_locations, state, world, player): def create_key_counters(key_layout, world, player): key_counters = {} + key_layout.found_doors.clear() flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) if world.doorShuffle[player] == 'vanilla': @@ -1403,6 +1456,9 @@ def create_key_counters(key_layout, world, player): while len(queue) > 0: next_key_counter, parent_state = queue.popleft() for door in next_key_counter.child_doors: + key_layout.found_doors.add(door) + if door.dest in flat_proposal and door.type != DoorType.SpiralStairs: + key_layout.found_doors.add(door.dest) child_state = parent_state.copy() if door.bigKey or door.name in special_big_key_doors: key_layout.key_logic.bk_doors.add(door) @@ -1520,11 +1576,11 @@ def find_counter_hint(opened_doors, bk_hint, key_layout): def find_max_counter(key_layout): - max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout) if max_counter is None: raise Exception("Max Counter is none - something is amiss") if len(max_counter.child_doors) > 0: - max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout) return max_counter diff --git a/Main.py b/Main.py index 9600fa8f..74d4c77e 100644 --- a/Main.py +++ b/Main.py @@ -448,6 +448,7 @@ def copy_world(world): # these need to be modified properly by set_rules new_location.access_rule = lambda state: True new_location.item_rule = lambda state: True + new_location.forced_item = location.forced_item # copy remaining itempool. No item in itempool should have an assigned location for item in world.itempool: diff --git a/Rules.py b/Rules.py index 3762b9bb..b30780ed 100644 --- a/Rules.py +++ b/Rules.py @@ -3,7 +3,7 @@ import logging from collections import deque import OverworldGlitchRules -from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier +from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType from RoomData import DoorKind from OverworldGlitchRules import overworld_glitches_rules @@ -1939,14 +1939,10 @@ bunny_impassible_doors = { def add_key_logic_rules(world, player): key_logic = world.key_logic[player] for d_name, d_logic in key_logic.items(): - for door_name, keys in d_logic.door_rules.items(): - spot = world.get_entrance(door_name, player) - if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot): - rule = create_advanced_key_rule(d_logic, player, keys) - if keys.opposite: - rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite)) - add_rule(spot, rule) - + for door_name, rule in d_logic.door_rules.items(): + add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) + if rule.allow_small: + set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -1954,8 +1950,9 @@ def add_key_logic_rules(world, player): forbid_item(location, d_logic.small_key_name, player) for door in d_logic.bk_doors: add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) - for chest in d_logic.bk_chests: - add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1: + for chest in d_logic.bk_chests: + add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: @@ -1963,6 +1960,39 @@ def add_key_logic_rules(world, player): add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) +def allow_self_locking_small(logic, player): + return lambda state, item: item.player == player and logic.small_key_name == item.name + + +def eval_small_key_door_main(state, door_name, dungeon, player): + if state.is_door_open(door_name, player): + return True + key_logic = state.world.key_logic[player][dungeon] + door_rule = key_logic.door_rules[door_name] + door_openable = False + for ruleType, number in door_rule.new_rules.items(): + if door_openable: + return True + if ruleType == KeyRuleType.WorstCase: + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) + elif ruleType == KeyRuleType.AllowSmall: + if door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name: + return True # always okay if allow small is on + elif isinstance(ruleType, tuple): + lock, lock_item = ruleType + # this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't + for loc in door_rule.alternate_big_key_loc: + spot = state.world.get_location(loc, player) + if spot.item and spot.item.name == lock_item: + door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) + break + return door_openable + + +def eval_small_key_door(door_name, dungeon, player): + return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) + + def retro_in_hc(spot): return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False diff --git a/Utils.py b/Utils.py index cf3db3fb..b8cf5497 100644 --- a/Utils.py +++ b/Utils.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 import os import re +import operator as op import subprocess import sys import xml.etree.ElementTree as ET from collections import defaultdict +from functools import reduce def int16_as_bytes(value): @@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap return "New Rom Hash: " + basemd5.hexdigest() +def kth_combination(k, l, r): + if r == 0: + return [] + elif len(l) == r: + return l + else: + i = ncr(len(l)-1, r-1) + if k < i: + return l[0:1] + kth_combination(k, l[1:], r-1) + else: + return kth_combination(k-i, l[1:], r) + + +def ncr(n, r): + if r == 0: + return 1 + r = min(r, n-r) + numerator = reduce(op.mul, range(n, n-r, -1), 1) + denominator = reduce(op.mul, range(1, r+1), 1) + return numerator / denominator + + entrance_offsets = { 'Sanctuary': 0x2, 'HC West': 0x3, From f15832f85ece904d29f7ea288e9a2317a46c0f9f Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 1 Jul 2021 16:00:47 -0600 Subject: [PATCH 002/104] Refinement - Missing blocked_connections added - appropriate checklists Optimization - Exp Cache - which is somewhat problematic with the playthrough logic --- BaseClasses.py | 239 ++++++++++++++++++++++++++++++++++------------ Fill.py | 10 +- KeyDoorShuffle.py | 1 + Main.py | 6 ++ Rules.py | 3 +- 5 files changed, 187 insertions(+), 72 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 54a45817..cab3c5c7 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -138,6 +138,8 @@ class World(object): set_player_attr('standardize_palettes', 'standardize') set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) + set_player_attr('exp_cache', defaultdict(dict)) + 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)})' @@ -392,6 +394,10 @@ class World(object): def clear_location_cache(self): self._cached_locations = None + def clear_exp_cache(self): + for p in range(1, self.players + 1): + self.exp_cache[p].clear() + def get_unfilled_locations(self, player=None): return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None] @@ -478,6 +484,7 @@ class CollectionState(object): self.ghost_keys = Counter() self.dungeon_limits = None + # self.trace = None def update_reachable_regions(self, player): self.stale[player] = False @@ -588,6 +595,8 @@ class CollectionState(object): def check_key_doors_in_dungeons(self, rrp, player): for dungeon_name, checklist in self.dungeons_to_check[player].items(): + if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist): + continue init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal remaining_keys = key_total - self.door_counter[player][1][dungeon_name] @@ -602,61 +611,64 @@ class CollectionState(object): child_states.append(self) visited_opened_doors = set() visited_opened_doors.add(frozenset(self.opened_doors[player])) - terminal_states, done, common_regions, common_bc, common_doors = [], False, {}, {}, set() - while not done: - terminal_states.clear() - while len(child_states) > 0: - next_child = child_states.popleft() - door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) - if door_candidates: - for chosen_door in door_candidates: - child_state = next_child.copy() - child_queue = deque() - child_state.door_counter[player][1][dungeon_name] += 1 - if isinstance(chosen_door, tuple): - child_state.opened_doors[player].add(chosen_door[0]) - child_state.opened_doors[player].add(chosen_door[1]) - if chosen_door[0] in checklist: - child_queue.append(checklist[chosen_door[0]]) - if chosen_door[1] in checklist: - child_queue.append(checklist[chosen_door[1]]) - else: - child_state.opened_doors[player].add(chosen_door) - if chosen_door in checklist: - child_queue.append(checklist[chosen_door]) + terminal_states, common_regions, common_bc, common_doors = [], {}, {}, set() + while len(child_states) > 0: + next_child = child_states.popleft() + door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player) + child_checklist = next_child.dungeons_to_check[player][dungeon_name] + if door_candidates: + for chosen_door in door_candidates: + child_state = next_child.copy() + child_queue = deque() + child_state.door_counter[player][1][dungeon_name] += 1 + if isinstance(chosen_door, tuple): + child_state.opened_doors[player].add(chosen_door[0]) + child_state.opened_doors[player].add(chosen_door[1]) + if chosen_door[0] in child_checklist: + child_queue.append(child_checklist[chosen_door[0]]) + if chosen_door[1] in child_checklist: + child_queue.append(child_checklist[chosen_door[1]]) + else: + child_state.opened_doors[player].add(chosen_door) + if chosen_door in child_checklist: + child_queue.append(child_checklist[chosen_door]) + if child_state.opened_doors[player] not in visited_opened_doors: + done = False + while not done: + rrp_ = child_state.reachable_regions[player] + bc_ = child_state.blocked_connections[player] + self.dungeon_limits = [dungeon_name] + child_state.traverse_world(child_queue, rrp_, bc_, player) + new_events = child_state.sweep_for_events_once() + child_state.stale[player] = False + if new_events: + for conn in bc_: + if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: + child_queue.append((conn, bc_[conn])) + done = not new_events if child_state.opened_doors[player] not in visited_opened_doors: - done = False - while not done: - rrp_ = child_state.reachable_regions[player] - bc_ = child_state.blocked_connections[player] - self.dungeon_limits = [dungeon_name] - child_state.traverse_world(child_queue, rrp_, bc_, player) - new_events = child_state.sweep_for_events_once() - child_state.stale[player] = False - if new_events: - for conn in bc_: - if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name: - child_queue.append((conn, bc_[conn])) - done = not new_events visited_opened_doors.add(frozenset(child_state.opened_doors[player])) child_states.append(child_state) - else: - terminal_states.append(next_child) - common_regions, common_doors, first = {}, set(), True - for term_state in terminal_states: - t_rrp = term_state.reachable_regions[player] - if first: - first = False - common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} - common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] - if valid_d_door(x)} - else: - cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} - common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() - if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} - common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] - if valid_d_door(x)} - done = len(child_states) == 0 + else: + terminal_states.append(next_child) + common_regions, common_bc, common_doors, first = {}, {}, set(), True + bc = self.blocked_connections[player] + for term_state in terminal_states: + t_rrp = term_state.reachable_regions[player] + t_bc = term_state.blocked_connections[player] + if first: + first = False + common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_bc = {x: y for x, y in t_bc.items() if x not in bc} + common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} + else: + cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]} + common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items() + if k in cm_rrp and self.crys_agree(v, cm_rrp[k])} + common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc}) + common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player] + if valid_d_door(x)} terminal_queue = deque() for door in common_doors: @@ -669,6 +681,9 @@ class CollectionState(object): self.dungeon_limits = [dungeon_name] rrp_ = self.reachable_regions[player] bc_ = self.blocked_connections[player] + for block, crystal in bc_.items(): + if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player): + terminal_queue.append((block, crystal)) self.traverse_world(terminal_queue, rrp_, bc_, player) self.dungeon_limits = None @@ -676,6 +691,13 @@ class CollectionState(object): missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} for k in missing_regions: rrp[k] = missing_regions[k] + missing_bc = {} + for blocked, crystal in common_bc.items(): + if blocked not in bc and self.should_visit(blocked.connected_region, rrp, crystal, player): + missing_bc[blocked] = crystal + for k in missing_bc: + bc[k] = missing_bc[k] + self.record_dungeon_exploration(player, dungeon_name, checklist, common_doors, missing_regions, missing_bc) checklist.clear() @staticmethod @@ -737,7 +759,6 @@ class CollectionState(object): for player in range(1, self.world.players + 1)} ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)} ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)} - # todo: verify if this isn't copied deep enough ret.dungeons_to_check = { player: defaultdict(dict, {name: copy.copy(checklist) for name, checklist in self.dungeons_to_check[player].items()}) @@ -745,6 +766,97 @@ class CollectionState(object): ret.ghost_keys = self.ghost_keys.copy() return ret + def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist): + bc = self.blocked_connections[player] + ec = self.world.exp_cache[player] + prog_set = self.reduce_prog_items(player, dungeon_name) + exp_key = (prog_set, frozenset(checklist)) + if dungeon_name in ec and exp_key in ec[dungeon_name]: + # apply + cnt, miss, common_doors, missing_regions, missing_bc = ec[dungeon_name][exp_key] + terminal_queue = deque() + for door in common_doors: + self.opened_doors[player].add(door) + if door in checklist: + terminal_queue.append(checklist[door]) + if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + self.door_counter[player][1][dungeon_name] += 1 + + self.dungeon_limits = [dungeon_name] + rrp_ = self.reachable_regions[player] + bc_ = self.blocked_connections[player] + for block, crystal in bc_.items(): + if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player): + terminal_queue.append((block, crystal)) + self.traverse_world(terminal_queue, rrp_, bc_, player) + self.dungeon_limits = None + + for k in missing_regions: + rrp[k] = missing_regions[k] + for k in missing_bc: + bc[k] = missing_bc[k] + + return True + return False + + def record_dungeon_exploration(self, player, dungeon_name, checklist, common_doors, missing_regions, missing_bc): + ec = self.world.exp_cache[player] + prog_set = self.reduce_prog_items(player, dungeon_name) + exp_key = (prog_set, frozenset(checklist)) + count = 1 + misses = 0 + # if exp_key in ec: + # if dungeon_name in ec[exp_key]: + # cnt, miss, old_common, old_missing, old_bc, trace = ec[exp_key][dungeon_name] + # if old_common == common_doors and old_missing == missing_regions and old_bc == missing_bc: + # count = cnt + 1 + # else: + # misses = miss + 1 + ec[dungeon_name][exp_key] = (count, misses, common_doors, missing_regions, missing_bc) + + def reduce_prog_items(self, player, dungeon_name): + # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc + # static logic rules needed most likely + # todo: universal smalls where needed + life_count, bottle_count = 0, 0 + reduced = Counter() + for item, cnt in self.prog_items.items(): + item_name, item_player = item + if item_player == player and self.check_if_progressive(item_name): + if item_name.startswith('Bottle'): + bottle_count += cnt + elif item_name.startswith(('Small Key', 'Big Key')): + d_name = 'Escape' if dungeon_name == 'Hyrule Castle' else dungeon_name + if d_name in item_name: + reduced[item] = cnt + elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: + if 'Container' in item_name: + life_count += 1 + elif 'Piece of Heart' == item_name: + life_count += .25 + else: + reduced[item] = cnt + if bottle_count > 0: + reduced[('Bottle', player)] = 1 + if life_count >= 1: + reduced[('Heart Container', player)] = 1 + return frozenset(reduced.items()) + + @staticmethod + def check_if_progressive(item_name): + return (item_name in + ['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot', + 'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel', + 'Lamp', 'Magic Powder', 'Moon Pearl', 'Cane of Somaria', 'Fire Rod', 'Flippers', 'Ice Rod', + 'Titans Mitts', 'Bombos', 'Ether', 'Quake', 'Master Sword', 'Tempered Sword', 'Fighter Sword', + 'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant', + 'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', + 'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield', + 'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', + 'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)', + 'Magic Upgrade (1/4)'] + or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))) + def can_reach(self, spot, resolution_hint=None, player=None): try: spot_type = spot.spot_type @@ -760,13 +872,10 @@ class CollectionState(object): return spot.can_reach(self) - def sweep_for_events_once(self, key_only=False, locations=None): - if locations is None: - locations = self.world.get_filled_locations() + def sweep_for_events_once(self): + locations = self.world.get_filled_locations() checked_locations = set([l for l in locations if l in self.locations_checked]) - reachable_events = [location for location in locations if location.event and - (not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey)) - and location.can_reach(self)] + reachable_events = [location for location in locations if location.event and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) for event in reachable_events: if event not in checked_locations: @@ -1958,9 +2067,7 @@ class Location(object): return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state))) def can_reach(self, state): - if self.parent_region.can_reach(state) and self.access_rule(state): - return True - return False + return self.parent_region.can_reach(state) and self.access_rule(state) def forced_big_key(self): if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player: @@ -1987,6 +2094,12 @@ class Location(object): world = self.parent_region.world if self.parent_region and self.parent_region.world else None return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' + def __eq__(self, other): + return self.name == other.name and self.player == other.player + + def __hash__(self): + return hash((self.name, self.player)) + class Item(object): @@ -2597,7 +2710,7 @@ class Settings(object): @unique -class KeyRuleType(Enum): +class KeyRuleType(FastEnum): WorstCase = 0 AllowSmall = 1 Lock = 2 diff --git a/Fill.py b/Fill.py index 9c914726..bb608cdb 100644 --- a/Fill.py +++ b/Fill.py @@ -199,7 +199,6 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = spot_to_fill = None - valid_locations = [] for location in locations: if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place @@ -210,16 +209,11 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = if (not single_player_placement or location.player == item_to_place.player)\ and location.can_fill(test_state, item_to_place, perform_access_check)\ and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - # todo: optimization: break instead of cataloging all valid locations - if not spot_to_fill: spot_to_fill = location - valid_locations.append(location) - + break if item_to_place.smallkey or item_to_place.bigkey: location.item = None - logging.getLogger('').debug(f'{item_to_place} valid placement at {len(valid_locations)} locations') - if spot_to_fill is None: # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.insert(0, item_to_place) @@ -691,7 +685,7 @@ def balance_money_progression(world): for player in solvent: wallet[player] -= sphere_costs[player] for location in locked_by_money[player]: - if location == 'Kiki': + if isinstance(location, str) and location == 'Kiki': kiki_paid[player] = True else: state.collect(location.item, True, location) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 857ee479..5732d8cd 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -795,6 +795,7 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location: rule.allow_small = True rule.small_location = next(iter(counter.free_locations)) + rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1 def find_inverted_counter(door, parent_counter, key_layout, world, player): diff --git a/Main.py b/Main.py index 74d4c77e..c72bcef6 100644 --- a/Main.py +++ b/Main.py @@ -389,6 +389,8 @@ def copy_world(world): ret.mixed_travel = world.mixed_travel.copy() ret.standardize_palettes = world.standardize_palettes.copy() + ret.exp_cache = world.exp_cache.copy() + for player in range(1, world.players + 1): if world.mode[player] != 'inverted': create_regions(ret, player) @@ -557,10 +559,14 @@ def create_playthrough(world): logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) old_item = location.item location.item = None + # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic + world.clear_exp_cache() if world.can_beat_game(state_cache[num]): + # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.append(location) else: # still required, got to keep it around + # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end diff --git a/Rules.py b/Rules.py index b30780ed..d8796dab 100644 --- a/Rules.py +++ b/Rules.py @@ -1976,7 +1976,8 @@ def eval_small_key_door_main(state, door_name, dungeon, player): if ruleType == KeyRuleType.WorstCase: door_openable |= state.has_sm_key(key_logic.small_key_name, player, number) elif ruleType == KeyRuleType.AllowSmall: - if door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name: + if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name + and door_rule.small_location.item.player == player): return True # always okay if allow small is on elif isinstance(ruleType, tuple): lock, lock_item = ruleType From 7fde95e5d7ee4a6339c8dd15e0d370935d3fb15f Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 2 Jul 2021 16:13:02 -0600 Subject: [PATCH 003/104] Small refinements for key logic --- BaseClasses.py | 35 +++++++++++++++------------------- Fill.py | 4 ++-- Main.py | 2 +- Rules.py | 6 ------ resources/app/cli/lang/en.json | 2 +- 5 files changed, 19 insertions(+), 30 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index cab3c5c7..2c41fbfb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -689,15 +689,20 @@ class CollectionState(object): rrp = self.reachable_regions[player] missing_regions = {x: y for x, y in common_regions.items() if x not in rrp} + paths = {} for k in missing_regions: rrp[k] = missing_regions[k] + possible_path = terminal_states[0].path[k] + self.path[k] = paths[k] = possible_path missing_bc = {} for blocked, crystal in common_bc.items(): - if blocked not in bc and self.should_visit(blocked.connected_region, rrp, crystal, player): + if (blocked not in bc and blocked.parent_region in rrp + and self.should_visit(blocked.connected_region, rrp, crystal, player)): missing_bc[blocked] = crystal for k in missing_bc: bc[k] = missing_bc[k] - self.record_dungeon_exploration(player, dungeon_name, checklist, common_doors, missing_regions, missing_bc) + self.record_dungeon_exploration(player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths) checklist.clear() @staticmethod @@ -773,7 +778,7 @@ class CollectionState(object): exp_key = (prog_set, frozenset(checklist)) if dungeon_name in ec and exp_key in ec[dungeon_name]: # apply - cnt, miss, common_doors, missing_regions, missing_bc = ec[dungeon_name][exp_key] + common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] terminal_queue = deque() for door in common_doors: self.opened_doors[player].add(door) @@ -793,42 +798,32 @@ class CollectionState(object): for k in missing_regions: rrp[k] = missing_regions[k] + for r, path in paths.items(): + self.path[r] = path for k in missing_bc: bc[k] = missing_bc[k] return True return False - def record_dungeon_exploration(self, player, dungeon_name, checklist, common_doors, missing_regions, missing_bc): + def record_dungeon_exploration(self, player, dungeon_name, checklist, + common_doors, missing_regions, missing_bc, paths): ec = self.world.exp_cache[player] prog_set = self.reduce_prog_items(player, dungeon_name) exp_key = (prog_set, frozenset(checklist)) - count = 1 - misses = 0 - # if exp_key in ec: - # if dungeon_name in ec[exp_key]: - # cnt, miss, old_common, old_missing, old_bc, trace = ec[exp_key][dungeon_name] - # if old_common == common_doors and old_missing == missing_regions and old_bc == missing_bc: - # count = cnt + 1 - # else: - # misses = miss + 1 - ec[dungeon_name][exp_key] = (count, misses, common_doors, missing_regions, missing_bc) + ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths) def reduce_prog_items(self, player, dungeon_name): # todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc - # static logic rules needed most likely + # cross dungeon requirements may be necessary for keysanity - which invalidates the above # todo: universal smalls where needed life_count, bottle_count = 0, 0 reduced = Counter() for item, cnt in self.prog_items.items(): item_name, item_player = item if item_player == player and self.check_if_progressive(item_name): - if item_name.startswith('Bottle'): + if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles bottle_count += cnt - elif item_name.startswith(('Small Key', 'Big Key')): - d_name = 'Escape' if dungeon_name == 'Hyrule Castle' else dungeon_name - if d_name in item_name: - reduced[item] = cnt elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']: if 'Container' in item_name: life_count += 1 diff --git a/Fill.py b/Fill.py index bb608cdb..ee13e990 100644 --- a/Fill.py +++ b/Fill.py @@ -209,8 +209,8 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = if (not single_player_placement or location.player == item_to_place.player)\ and location.can_fill(test_state, item_to_place, perform_access_check)\ and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): - spot_to_fill = location - break + spot_to_fill = location + break if item_to_place.smallkey or item_to_place.bigkey: location.item = None diff --git a/Main.py b/Main.py index c72bcef6..78fcaec8 100644 --- a/Main.py +++ b/Main.py @@ -27,7 +27,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.4.0.8-u' +__version__ = '0.4.1.0-u' class EnemizerError(RuntimeError): diff --git a/Rules.py b/Rules.py index d8796dab..3cd18b2b 100644 --- a/Rules.py +++ b/Rules.py @@ -1941,8 +1941,6 @@ def add_key_logic_rules(world, player): for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): add_rule(world.get_entrance(door_name, player), eval_small_key_door(door_name, d_name, player)) - if rule.allow_small: - set_always_allow(rule.small_location, allow_self_locking_small(d_logic, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -1960,10 +1958,6 @@ def add_key_logic_rules(world, player): add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1)) -def allow_self_locking_small(logic, player): - return lambda state, item: item.player == player and logic.small_key_name == item.name - - def eval_small_key_door_main(state, door_name, dungeon, player): if state.is_door_open(door_name, player): return True diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 145667ef..fa7c355e 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -2,7 +2,7 @@ "cli": { "yes": "Yes", "no": "No", - "app.title": "ALttP Door Randomizer Version %s - Seed: %d, Code: %s", + "app.title": "ALttP Door Randomizer Version %s : --seed %d --code %s", "version": "Version", "seed": "Seed", "player": "Player", From 96e4ba3e2ed5bffafa2641e86ed5b665b465c23d Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 6 Jul 2021 13:20:28 -0700 Subject: [PATCH 004/104] Insanity: blacksmith/bomb shop placement shouldn't preclude another multi-exit cave from exiting there --- EntranceShuffle.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e78b35b8..e111ddff 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -963,7 +963,6 @@ def link_entrances(world, player): blacksmith_hut = blacksmith_doors.pop() connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) - exit_pool.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] @@ -971,7 +970,6 @@ def link_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) doors.remove(bomb_shop) - exit_pool.remove(bomb_shop) # handle remaining caves for cave in caves: @@ -1804,7 +1802,6 @@ def link_inverted_entrances(world, player): blacksmith_hut = blacksmith_doors.pop() connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) doors.remove(blacksmith_hut) - exit_pool.remove(blacksmith_hut) # place dam and pyramid fairy, have limited options bomb_shop_doors = [door for door in bomb_shop_doors if door in doors] @@ -1812,7 +1809,6 @@ def link_inverted_entrances(world, player): bomb_shop = bomb_shop_doors.pop() connect_entrance(world, bomb_shop, 'Inverted Big Bomb Shop', player) doors.remove(bomb_shop) - exit_pool.remove(bomb_shop) # handle remaining caves for cave in caves: From 8c2b6f3bd2165489353ac4e26c460b9107292e4c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 12 Jul 2021 15:54:38 -0700 Subject: [PATCH 005/104] Can't have a key at blind unless you can crack the floor and deliver the maiden --- KeyDoorShuffle.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5732d8cd..89fa307d 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1035,14 +1035,6 @@ def filter_big_chest(locations): return [x for x in locations if '- Big Chest' not in x.name] -def count_locations_exclude_logic(locations, key_logic): - cnt = 0 - for loc in locations: - if not location_is_bk_locked(loc, key_logic) and not loc.forced_item and not prize_or_event(loc): - cnt += 1 - return cnt - - def location_is_bk_locked(loc, key_logic): return loc in key_logic.bk_chests or loc in key_logic.bk_locked @@ -1051,18 +1043,36 @@ def prize_or_event(loc): return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] -def count_free_locations(state): +def boss_unavail(loc, world, player): + # todo: ambrosia + # return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name + return False + + +def blind_boss_unavail(loc, state, world, player): + if loc.name == "Thieves' Town - Boss": + # todo: check attic + return (loc.parent_region.dungeon.boss.name == 'Blind' and + (not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or + (world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and + not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor')))) + return False + + +def count_free_locations(state, world, player): cnt = 0 for loc in state.found_locations: - if not prize_or_event(loc) and not loc.forced_item: + if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player) + and not blind_boss_unavail(loc, state, world, player)): cnt += 1 return cnt -def count_locations_exclude_big_chest(state): +def count_locations_exclude_big_chest(state, world, player): cnt = 0 for loc in state.found_locations: - if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc): + if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player) + and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)): cnt += 1 return cnt @@ -1340,7 +1350,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not smalls_avail and num_bigs == 0: return True # I think that's the end # todo: fix state to separate out these types - ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state) + if state.big_key_opened: + ttl_locations = count_free_locations(state, world, player) + else: + ttl_locations = count_locations_exclude_big_chest(state, world, player) ttl_small_key_only = count_small_key_only_locations(state) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) From 9ef24a610d1defea451c5a94716b0508ace0ebef Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 13 Jul 2021 10:38:14 -0700 Subject: [PATCH 006/104] Bug with key flooding (events that would flood the key are not unresolved) Last ditch placement efforts for when things get stuck (crossed standard mostly) --- BaseClasses.py | 1 + Fill.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index c4257fa1..7a3fe9e8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -505,6 +505,7 @@ class CollectionState(object): unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) and x not in self.locations_checked and x.can_reach(self)] + unresolved_events = self._do_not_flood_the_keys(unresolved_events) if len(unresolved_events) == 0: self.check_key_doors_in_dungeons(rrp, player) diff --git a/Fill.py b/Fill.py index d2480df8..2a578ff7 100644 --- a/Fill.py +++ b/Fill.py @@ -221,7 +221,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = if world.accessibility[item_to_place.player] != 'none': logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) continue - raise FillError('No more spots to place %s' % item_to_place) + spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, + base_state, itempool, keys_in_itempool, single_player_placement) + if spot_to_fill is None: + raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) track_outside_keys(item_to_place, spot_to_fill, world) @@ -258,6 +261,69 @@ def track_outside_keys(item, location, world): world.key_logic[item.player][item_dungeon].outside_keys += 1 +def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, + keys_in_itempool=None, single_player_placement=False): + def location_preference(loc): + if not loc.item.advancement: + return 1 + if loc.item.type and loc.item.type != 'Sword': + if loc.item.type in ['Map', 'Compass']: + return 2 + else: + return 3 + return 4 + + possible_swaps = [x for x in state.locations_checked + if x.item.type not in ['Event', 'Crystal']] + swap_locations = sorted(possible_swaps, key=location_preference) + + for location in swap_locations: + old_item = location.item + new_pool = list(itempool) + [old_item] + new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, + keys_in_itempool, single_player_placement) + if new_spot: + new_spot.item = item_to_place + swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, + keys_in_itempool, single_player_placement) + if swap_spot: + logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') + world.push_item(swap_spot, old_item, False) + locations.remove(swap_spot) + locations.append(new_spot) + return new_spot + return None + + +def find_spot_for_item(item_to_place, locations, world, base_state, pool, + keys_in_itempool=None, single_player_placement=False): + def sweep_from_pool(): + new_state = base_state.copy() + for item in pool: + new_state.collect(item, True) + new_state.sweep_for_events() + return new_state + for location in locations: + maximum_exploration_state = sweep_from_pool() + perform_access_check = True + if world.accessibility[item_to_place.player] == 'none': + perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state) + + if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + location.item = item_to_place + test_state = maximum_exploration_state.copy() + test_state.stale[item_to_place.player] = True + else: + test_state = maximum_exploration_state + if (not single_player_placement or location.player == item_to_place.player) \ + and location.can_fill(test_state, item_to_place, perform_access_check) \ + and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): + return location + if item_to_place.smallkey or item_to_place.bigkey: + location.item = None + return None + + def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None): # If not passed in, then get a shuffled list of locations to fill in if not fill_locations: From b56f8d4136259ce1566a3be83a24f6aeaed391b4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 15 Jul 2021 08:03:33 -0700 Subject: [PATCH 007/104] Std+Retro: key logic for escape and exclude some bow logic sectors Bug with reachable doors --- BaseClasses.py | 48 +++++++++++++++++++++++++++++++-------------- DoorShuffle.py | 6 +++--- DungeonGenerator.py | 23 +++++++++++++++++++++- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7a3fe9e8..01179702 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -482,8 +482,6 @@ class CollectionState(object): self.reached_doors = {player: set() for player in range(1, parent.players + 1)} self.opened_doors = {player: set() for player in range(1, parent.players + 1)} self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)} - - self.ghost_keys = Counter() self.dungeon_limits = None # self.trace = None @@ -551,9 +549,9 @@ class CollectionState(object): if (new_entrance, new_crystal_state) not in queue: queue.append((new_entrance, new_crystal_state)) # else those connections that are not accessible yet - if self.is_small_door(connection) and not self.world.retro[player]: # todo: retro + if self.is_small_door(connection): door = connection.door - dungeon_name = connection.parent_region.dungeon.name # todo: universal + dungeon_name = connection.parent_region.dungeon.name key_logic = self.world.key_logic[player][dungeon_name] if door.name not in self.reached_doors[player]: self.door_counter[player][0][dungeon_name] += 1 @@ -561,7 +559,8 @@ class CollectionState(object): if key_logic.sm_doors[door]: self.reached_doors[player].add(key_logic.sm_doors[door].name) if not connection.can_reach(self): - checklist = self.dungeons_to_check[player][dungeon_name] + checklist_key = 'Universal' if self.world.retro[player] else dungeon_name + checklist = self.dungeons_to_check[player][checklist_key] checklist[connection.name] = (connection, crystal_state) elif door.name not in self.opened_doors[player]: opened_doors = self.opened_doors[player] @@ -639,7 +638,7 @@ class CollectionState(object): while not done: rrp_ = child_state.reachable_regions[player] bc_ = child_state.blocked_connections[player] - self.dungeon_limits = [dungeon_name] + self.set_dungeon_limits(player, dungeon_name) child_state.traverse_world(child_queue, rrp_, bc_, player) new_events = child_state.sweep_for_events_once() child_state.stale[player] = False @@ -674,13 +673,19 @@ class CollectionState(object): terminal_queue = deque() for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) + if door not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door) + if pair not in self.reached_doors[player]: + self.reached_doors[player].add(pair) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) - if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + if pair not in self.opened_doors[player]: self.door_counter[player][1][dungeon_name] += 1 - self.dungeon_limits = [dungeon_name] + self.set_dungeon_limits(player, dungeon_name) rrp_ = self.reachable_regions[player] bc_ = self.blocked_connections[player] for block, crystal in bc_.items(): @@ -722,14 +727,20 @@ class CollectionState(object): return paired_door.name if paired_door else None return None + def set_dungeon_limits(self, player, dungeon_name): + if self.world.retro[player] and self.world.mode[player] == 'standard': + self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower'] + else: + self.dungeon_limits = [dungeon_name] + @staticmethod def should_explore_child_state(state, dungeon_name, player): - small_key_name = dungeon_keys[dungeon_name] # todo: universal - key_total = state.prog_items[(small_key_name, player)] + state.ghost_keys[(small_key_name, player)] + small_key_name = dungeon_keys[dungeon_name] + key_total = state.prog_items[(small_key_name, player)] remaining_keys = key_total - state.door_counter[player][1][dungeon_name] unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name] if remaining_keys > 0 and unopened_doors > 0: - key_logic = state.world.key_logic[player][dungeon_name] # todo: universal + key_logic = state.world.key_logic[player][dungeon_name] door_candidates, skip = [], set() for door, paired in key_logic.sm_doors.items(): if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]: @@ -770,7 +781,6 @@ class CollectionState(object): player: defaultdict(dict, {name: copy.copy(checklist) for name, checklist in self.dungeons_to_check[player].items()}) for player in range(1, self.world.players + 1)} - ret.ghost_keys = self.ghost_keys.copy() return ret def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist): @@ -783,13 +793,19 @@ class CollectionState(object): common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key] terminal_queue = deque() for door in common_doors: + pair = self.find_door_pair(player, dungeon_name, door) + if door not in self.reached_doors[player]: + self.door_counter[player][0][dungeon_name] += 1 + self.reached_doors[player].add(door) + if pair not in self.reached_doors[player]: + self.reached_doors[player].add(pair) self.opened_doors[player].add(door) if door in checklist: terminal_queue.append(checklist[door]) - if self.find_door_pair(player, dungeon_name, door) not in self.opened_doors[player]: + if pair not in self.opened_doors[player]: self.door_counter[player][1][dungeon_name] += 1 - self.dungeon_limits = [dungeon_name] + self.set_dungeon_limits(player, dungeon_name) rrp_ = self.reachable_regions[player] bc_ = self.blocked_connections[player] for block, crystal in bc_.items(): @@ -1812,6 +1828,7 @@ class Sector(object): self.entrance_sector = None self.destination_entrance = False self.equations = None + self.item_logic = set() def region_set(self): if self.r_name_set is None: @@ -2552,7 +2569,8 @@ dungeon_keys = { 'Ice Palace': 'Small Key (Ice Palace)', 'Misery Mire': 'Small Key (Misery Mire)', 'Turtle Rock': 'Small Key (Turtle Rock)', - 'Ganons Tower': 'Small Key (Ganons Tower)' + 'Ganons Tower': 'Small Key (Ganons Tower)', + 'Universal': 'Small Key (Universal)' } class PotItem(FastEnum): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ce078f0..5231fc91 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1101,9 +1101,9 @@ def assign_cross_keys(dungeon_builders, world, player): logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining) # Last Step: Adjust Small Key Dungeon Pool - if not world.retro[player]: - for name, builder in dungeon_builders.items(): - reassign_key_doors(builder, world, player) + for name, builder in dungeon_builders.items(): + reassign_key_doors(builder, world, player) + if not world.retro[player]: log_key_logic(builder.name, world.key_logic[player][builder.name]) actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0) dungeon = world.get_dungeon(name, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index c8599748..9d76ddb5 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1290,13 +1290,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, sanc_builder = random.choice(lw_builders) assign_sector(sanc, sanc_builder, candidate_sectors, global_pole) + bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard' free_location_sectors = {} crystal_switches = {} crystal_barriers = {} polarized_sectors = {} neutral_sectors = {} for sector in candidate_sectors: - if sector.chest_locations > 0: + if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC + bow_sectors[sector] = None + elif sector.chest_locations > 0: free_location_sectors[sector] = None elif sector.c_switch: crystal_switches[sector] = None @@ -1306,6 +1309,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, neutral_sectors[sector] = None else: polarized_sectors[sector] = None + if bow_sectors: + assign_bow_sectors(dungeon_map, bow_sectors, global_pole) assign_location_sectors(dungeon_map, free_location_sectors, global_pole) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) @@ -1471,6 +1476,9 @@ def define_sector_features(sectors): sector.blue_barrier = True if door.bigKey: sector.bk_required = True + if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2', + 'Eastern Single Eyegore', 'Eastern Duo Eyegores']: + sector.item_logic.add('Bow') def assign_sector(sector, dungeon, candidate_sectors, global_pole): @@ -1521,6 +1529,19 @@ def find_sector(r_name, sectors): return None +def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): + sector_list = list(bow_sectors) + random.shuffle(sector_list) + population = [] + for name in dungeon_map: + if name != 'Hyrule Castle': + population.append(name) + choices = random.choices(population, k=len(sector_list)) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + assign_sector(sector_list[i], builder, bow_sectors, global_pole) + + def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): valid = False choices = None From 68dc27902b46bbc7998490bf002287bdb667225f Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 15 Jul 2021 16:02:07 -0700 Subject: [PATCH 008/104] SFX Shuffle initial implementation --- Adjuster.py | 1 + AdjusterMain.py | 2 +- CLI.py | 3 +- Main.py | 3 +- Mystery.py | 1 + Rom.py | 7 +- mystery_example.yml | 3 + resources/app/cli/args.json | 4 + resources/app/cli/lang/en.json | 1 + .../app/gui/adjust/overview/widgets.json | 3 +- resources/app/gui/lang/en.json | 2 + .../gui/randomize/gameoptions/widgets.json | 3 +- source/classes/SFX.py | 191 ++++++++++++++++++ source/classes/constants.py | 3 +- source/gui/adjust/overview.py | 1 + 15 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 source/classes/SFX.py diff --git a/Adjuster.py b/Adjuster.py index c6f42e6e..a6e964a8 100755 --- a/Adjuster.py +++ b/Adjuster.py @@ -36,6 +36,7 @@ def main(): parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout']) parser.add_argument('--reduce_flashing', help='Reduce some in-game flashing.', action='store_true') + parser.add_argument('--shuffle_sfx', help='Shuffles sound sfx', action='store_true') parser.add_argument('--sprite', help='''\ Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes, diff --git a/AdjusterMain.py b/AdjusterMain.py index bc463444..7d7e2f6e 100644 --- a/AdjusterMain.py +++ b/AdjusterMain.py @@ -25,7 +25,7 @@ def adjust(args): args.sprite = None apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, - args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing) + args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing, args.shuffle_sfx) output_path.cached_path = args.outputpath rom.write_to_file(output_path('%s.sfc' % outfilebase)) diff --git a/CLI.py b/CLI.py index d87e1e2c..dc374769 100644 --- a/CLI.py +++ b/CLI.py @@ -102,7 +102,7 @@ def parse_cli(argv, no_defaults=False): 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', - 'reduce_flashing']: + 'reduce_flashing', 'shuffle_sfx']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -190,6 +190,7 @@ def parse_settings(): "ow_palettes": "default", "uw_palettes": "default", "reduce_flashing": False, + "shuffle_sfx": False, # Spoiler defaults to TRUE # Playthrough defaults to TRUE diff --git a/Main.py b/Main.py index eb8380f3..71ef017a 100644 --- a/Main.py +++ b/Main.py @@ -281,7 +281,8 @@ def main(args, seed=None, fish=None): apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], - args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player]) + args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player], + args.shuffle_sfx[player]) if args.jsonout: jsonout[f'patch_t{team}_p{player}'] = rom.patches diff --git a/Mystery.py b/Mystery.py index f82e64d2..8914c1be 100644 --- a/Mystery.py +++ b/Mystery.py @@ -231,6 +231,7 @@ def roll_settings(weights): ret.heartbeep = get_choice('heartbeep', romweights) ret.ow_palettes = get_choice('ow_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights) + ret.uw_palettes = get_choice('shuffle_sfx', romweights) == 'on' return ret diff --git a/Rom.py b/Rom.py index 5c61e5c2..0e2215a7 100644 --- a/Rom.py +++ b/Rom.py @@ -28,6 +28,8 @@ from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_ from Items import ItemFactory from EntranceShuffle import door_addresses, exit_ids +from source.classes.SFX import randomize_sfx + JAP10HASH = '03a63945398191337e896e5771f77173' RANDOMIZERBASEHASH = '25dd18672e1234c85900f5b2155e7e4f' @@ -1624,7 +1626,7 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, - ow_palettes, uw_palettes, reduce_flashing): + ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx): if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer: dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380], @@ -1727,6 +1729,9 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr elif uw_palettes == 'blackout': blackout_uw_palettes(rom) + if shuffle_sfx: + randomize_sfx(rom) + if isinstance(rom, LocalRom): rom.write_crc() diff --git a/mystery_example.yml b/mystery_example.yml index e349063d..028e7110 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -132,3 +132,6 @@ half: 0 quarter: 1 off: 0 + shuffle_sfx: + on: 1 + off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 47bb3987..dc4b917c 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -199,6 +199,10 @@ "action": "store_true", "type": "bool" }, + "shuffle_sfx": { + "action": "store_true", + "type": "bool" + }, "mapshuffle": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 0ff910a9..8c5c0f8c 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -295,6 +295,7 @@ "sprite that will be extracted." ], "reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ], + "shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ], "create_rom": [ "Create an output rom file. (default: %(default)s)" ], "gui": [ "Launch the GUI. (default: %(default)s)" ], "jsonout": [ diff --git a/resources/app/gui/adjust/overview/widgets.json b/resources/app/gui/adjust/overview/widgets.json index b61fff0e..85efcf1f 100644 --- a/resources/app/gui/adjust/overview/widgets.json +++ b/resources/app/gui/adjust/overview/widgets.json @@ -2,7 +2,8 @@ "checkboxes": { "nobgm": { "type": "checkbox" }, "quickswap": { "type": "checkbox" }, - "reduce_flashing": {"type": "checkbox"} + "reduce_flashing": {"type": "checkbox"}, + "shuffle_sfx": {"type": "checkbox"} }, "leftAdjustFrame": { "heartcolor": { diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 0d9e3836..24134897 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -3,6 +3,7 @@ "adjust.nobgm": "Disable Music & MSU-1", "adjust.quickswap": "L/R Quickswapping", "adjust.reduce_flashing": "Reduce Flashing", + "adjust.shuffle_sfx": "Shuffle Sound Effects", "adjust.heartcolor": "Heart Color", "adjust.heartcolor.red": "Red", @@ -134,6 +135,7 @@ "randomizer.gameoptions.nobgm": "Disable Music & MSU-1", "randomizer.gameoptions.quickswap": "L/R Quickswapping", "randomizer.gameoptions.reduce_flashing": "Reduce Flashing", + "randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects", "randomizer.gameoptions.heartcolor": "Heart Color", "randomizer.gameoptions.heartcolor.red": "Red", diff --git a/resources/app/gui/randomize/gameoptions/widgets.json b/resources/app/gui/randomize/gameoptions/widgets.json index 63556e0f..6efe32c8 100644 --- a/resources/app/gui/randomize/gameoptions/widgets.json +++ b/resources/app/gui/randomize/gameoptions/widgets.json @@ -2,7 +2,8 @@ "checkboxes": { "nobgm": { "type": "checkbox" }, "quickswap": { "type": "checkbox" }, - "reduce_flashing": {"type": "checkbox"} + "reduce_flashing": {"type": "checkbox"}, + "shuffle_sfx": {"type": "checkbox"} }, "leftRomOptionsFrame": { "heartcolor": { diff --git a/source/classes/SFX.py b/source/classes/SFX.py new file mode 100644 index 00000000..ed6d93f6 --- /dev/null +++ b/source/classes/SFX.py @@ -0,0 +1,191 @@ +import random +from Utils import int16_as_bytes + + +class SFX(object): + + def __init__(self, name, sfx_set, orig_id, addr, chain, accomp=False): + self.name = name + self.sfx_set = sfx_set + self.orig_id = orig_id + self.addr = addr + self.chain = chain + self.accomp = accomp + + self.target_set = None + self.target_id = None + self.target_chain = None + + +def init_sfx_data(): + sfx_pool = [SFX('Slash1', 0x02, 0x01, 0x2614, []), SFX('Slash2', 0x02, 0x02, 0x2625, []), + SFX('Slash3', 0x02, 0x03, 0x2634, []), SFX('Slash4', 0x02, 0x04, 0x2643, []), + SFX('Wall clink', 0x02, 0x05, 0x25DD, []), SFX('Bombable door clink', 0x02, 0x06, 0x25D7, []), + SFX('Fwoosh shooting', 0x02, 0x07, 0x25B7, []), SFX('Arrow hitting wall', 0x02, 0x08, 0x25E3, []), + SFX('Boomerang whooshing', 0x02, 0x09, 0x25AD, []), SFX('Hookshot', 0x02, 0x0A, 0x25C7, []), + SFX('Placing bomb', 0x02, 0x0B, 0x2478, []), + SFX('Bomb exploding/Quake/Bombos/Exploding wall', 0x02, 0x0C, 0x269C, []), + SFX('Powder', 0x02, 0x0D, 0x2414, [0x3f]), SFX('Fire rod shot', 0x02, 0x0E, 0x2404, []), + SFX('Ice rod shot', 0x02, 0x0F, 0x24C3, []), SFX('Hammer use', 0x02, 0x10, 0x23FA, []), + SFX('Hammering peg', 0x02, 0x11, 0x23F0, []), SFX('Digging', 0x02, 0x12, 0x23CD, []), + SFX('Flute use', 0x02, 0x13, 0x23A0, [0x3e]), SFX('Cape on', 0x02, 0x14, 0x2380, []), + SFX('Cape off/Wallmaster grab', 0x02, 0x15, 0x2390, []), SFX('Staircase', 0x02, 0x16, 0x232C, []), + SFX('Staircase', 0x02, 0x17, 0x2344, []), SFX('Staircase', 0x02, 0x18, 0x2356, []), + SFX('Staircase', 0x02, 0x19, 0x236E, []), SFX('Tall grass/Hammer hitting bush', 0x02, 0x1A, 0x2316, []), + SFX('Mire shallow water', 0x02, 0x1B, 0x2307, []), SFX('Shallow water', 0x02, 0x1C, 0x2301, []), + SFX('Lifting object', 0x02, 0x1D, 0x22BB, []), SFX('Cutting grass', 0x02, 0x1E, 0x2577, []), + SFX('Item breaking', 0x02, 0x1F, 0x22E9, []), SFX('Item falling in pit', 0x02, 0x20, 0x22DA, []), + SFX('Bomb hitting ground/General bang', 0x02, 0x21, 0x22CF, []), + SFX('Pushing object/Armos bounce', 0x02, 0x22, 0x2107, []), SFX('Boots dust', 0x02, 0x23, 0x22B1, []), + SFX('Splashing', 0x02, 0x24, 0x22A5, [0x3d]), SFX('Mire shallow water again?', 0x02, 0x25, 0x2296, []), + SFX('Link taking damage', 0x02, 0x26, 0x2844, []), SFX('Fainting', 0x02, 0x27, 0x2252, []), + SFX('Item splash', 0x02, 0x28, 0x2287, []), SFX('Rupee refill', 0x02, 0x29, 0x243F, [0x3b]), + SFX('Fire rod shot hitting wall/Bombos spell', 0x02, 0x2A, 0x2033, []), + SFX('Heart beep/Text box', 0x02, 0x2B, 0x1FF2, []), SFX('Sword up', 0x02, 0x2C, 0x1FD9, [0x3a]), + SFX('Magic drain', 0x02, 0x2D, 0x20A6, []), SFX('GT opening', 0x02, 0x2E, 0x1FCA, [0x39]), + SFX('GT opening/Water drain', 0x02, 0x2F, 0x1F47, [0x38]), SFX('Cucco', 0x02, 0x30, 0x1EF1, []), + SFX('Fairy', 0x02, 0x31, 0x20CE, []), SFX('Bug net', 0x02, 0x32, 0x1D47, []), + SFX('Teleport2', 0x02, 0x33, 0x1CDC, [], True), SFX('Teleport1', 0x02, 0x34, 0x1F6F, [0x33]), + SFX('Quake/Vitreous/Zora king/Armos/Pyramid/Lanmo', 0x02, 0x35, 0x1C67, [0x36]), + SFX('Mire entrance (extends above)', 0x02, 0x36, 0x1C64, [], True), + SFX('Spin charged', 0x02, 0x37, 0x1A43, []), SFX('Water sound', 0x02, 0x38, 0x1F6F, [], True), + SFX('GT opening thunder', 0x02, 0x39, 0x1F9C, [], True), SFX('Sword up', 0x02, 0x3A, 0x1FE7, [], True), + SFX('Quiet rupees', 0x02, 0x3B, 0x2462, [], True), SFX('Error beep', 0x02, 0x3C, 0x1A37, []), + SFX('Big splash', 0x02, 0x3D, 0x22AB, [], True), SFX('Flute again', 0x02, 0x3E, 0x23B5, [], True), + SFX('Powder paired', 0x02, 0x3F, 0x2435, [], True), + + SFX('Sword beam', 0x03, 0x01, 0x1A18, []), + SFX('TR opening', 0x03, 0x02, 0x254E, []), SFX('Pyramid hole', 0x03, 0x03, 0x224A, []), + SFX('Angry soldier', 0x03, 0x04, 0x220E, []), SFX('Lynel shot/Javelin toss', 0x03, 0x05, 0x25B7, []), + SFX('BNC swing/Phantom ganon/Helma tail/Arrghus swoosh', 0x03, 0x06, 0x21F5, []), + SFX('Cannon fire', 0x03, 0x07, 0x223D, []), SFX('Damage to enemy; $0BEX.4=1', 0x03, 0x08, 0x21E6, []), + SFX('Enemy death', 0x03, 0x09, 0x21C1, []), SFX('Collecting rupee', 0x03, 0x0A, 0x21A9, []), + SFX('Collecting heart', 0x03, 0x0B, 0x2198, []), + SFX('Non-blank text character', 0x03, 0x0C, 0x218E, []), + SFX('HUD heart (used explicitly by sanc heart?)', 0x03, 0x0D, 0x21B5, []), + SFX('Opening chest', 0x03, 0x0E, 0x2182, []), + SFX('♪Do do do doooooo♫', 0x03, 0x0F, 0x24B9, [0x3C, 0x3D, 0x3E, 0x3F]), + SFX('Opening/Closing map (paired)', 0x03, 0x10, 0x216D, [0x3b]), + SFX('Opening item menu/Bomb shop guy breathing', 0x03, 0x11, 0x214F, []), + SFX('Closing item menu/Bomb shop guy breathing', 0x03, 0x12, 0x215E, []), + SFX('Throwing object (sprites use it as well)/Stalfos jump', 0x03, 0x13, 0x213B, []), + SFX('Key door/Trinecks/Dash key landing/Stalfos Knight collapse', 0x03, 0x14, 0x246C, []), + SFX('Door closing/OW door opening/Chest opening (w/ $29 in $012E)', 0x03, 0x15, 0x212F, []), + SFX('Armos Knight thud', 0x03, 0x16, 0x2123, []), SFX('Rat squeak', 0x03, 0x17, 0x25A6, []), + SFX('Dragging/Mantle moving', 0x03, 0x18, 0x20DD, []), + SFX('Fireball/Laser shot; Somehow used by Trinexx???', 0x03, 0x19, 0x250A, []), + SFX('Chest reveal jingle ', 0x03, 0x1A, 0x1E8A, [0x38]), + SFX('Puzzle jingle', 0x03, 0x1B, 0x20B6, [0x3a]), SFX('Damage to enemy', 0x03, 0x1C, 0x1A62, []), + SFX('Potion refill/Magic drain', 0x03, 0x1D, 0x20A6, []), + SFX('Flapping (Duck/Cucco swarm/Ganon bats/Keese/Raven/Vulture)', 0x03, 0x1E, 0x2091, []), + SFX('Link falling', 0x03, 0x1F, 0x204B, []), SFX('Menu/Text cursor moved', 0x03, 0x20, 0x276C, []), + SFX('Damage to boss', 0x03, 0x21, 0x27E2, []), SFX('Boss dying/Deleting file', 0x03, 0x22, 0x26CF, []), + SFX('Spin attack/Medallion swoosh', 0x03, 0x23, 0x2001, [0x39]), + SFX('OW map perspective change', 0x03, 0x24, 0x2043, []), + SFX('Pressure switch', 0x03, 0x25, 0x1E9D, []), + SFX('Lightning/Game over/Laser/Ganon bat/Trinexx lunge', 0x03, 0x26, 0x1E7B, []), + SFX('Agahnim charge', 0x03, 0x27, 0x1E40, []), SFX('Agahnim/Ganon teleport', 0x03, 0x28, 0x26F7, []), + SFX('Agahnim shot', 0x03, 0x29, 0x1E21, []), + SFX('Somaria/Byrna/Ether spell/Helma fire ball', 0x03, 0x2A, 0x1E12, []), + SFX('Electrocution', 0x03, 0x2B, 0x1DF3, []), SFX('Bees', 0x03, 0x2C, 0x1DC0, []), + SFX('Milestone, also via text', 0x03, 0x2D, 0x1DA9, [0x37]), + SFX('Collecting heart container', 0x03, 0x2E, 0x1D5D, [0x35, 0x34]), + SFX('Collecting absorbable key', 0x03, 0x2F, 0x1D80, [0x33]), + SFX('Byrna spark/Item plop/Magic bat zap/Blob emerge', 0x03, 0x30, 0x1B53, []), + SFX('Sprite falling/Moldorm shuffle', 0x03, 0x31, 0x1ACA, []), + SFX('Bumper boing/Somaria punt/Blob transmutation/Sprite boings', 0x03, 0x32, 0x1A78, []), + SFX('Jingle (paired $2F→$33)', 0x03, 0x33, 0x1D93, [], True), + SFX('Depressing jingle (paired $2E→$35→$34)', 0x03, 0x34, 0x1D66, [], True), + SFX('Ugly jingle (paired $2E→$35→$34)', 0x03, 0x35, 0x1D73, [], True), + SFX('Wizzrobe shot/Helma fireball split/Mothula beam/Blue balls', 0x03, 0x36, 0x1AA7, []), + SFX('Dinky jingle (paired $2D→$37)', 0x03, 0x37, 0x1DB4, [], True), + SFX('Apathetic jingle (paired $1A→$38)', 0x03, 0x38, 0x1E93, [], True), + SFX('Quiet swish (paired $23→$39)', 0x03, 0x39, 0x2017, [], True), + SFX('Defective jingle (paired $1B→$3A)', 0x03, 0x3A, 0x20C0, [], True), + SFX('Petulant jingle (paired $10→$3B)', 0x03, 0x3B, 0x2176, [], True), + SFX('Triumphant jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3C, 0x248A, [], True), + SFX('Less triumphant jingle ($0F→$3C→$3D→$3E→$3F)', 0x03, 0x3D, 0x2494, [], True), + SFX('"You tried, I guess" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3E, 0x249E, [], True), + SFX('"You didn\'t really try" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3F, 0x2480, [], True)] + return sfx_pool + + +def shuffle_sfx_data(): + sfx_pool = init_sfx_data() + sfx_map = {2: {}, 3: {}} + accompaniment_map = {2: set(), 3: set()} + candidates = [] + for sfx in sfx_pool: + sfx_map[sfx.sfx_set][sfx.orig_id] = sfx + if not sfx.accomp: + candidates.append((sfx.sfx_set, sfx.orig_id)) + else: + accompaniment_map[sfx.sfx_set].add(sfx.orig_id) + chained_sfx = [x for x in sfx_pool if len(x.chain) > 0] + + random.shuffle(candidates) + + # place chained sfx first + random.shuffle(chained_sfx) # todo: sort largest to smallest + chained_sfx = sorted(chained_sfx, key=lambda x: len(x.chain), reverse=True) + for chained in chained_sfx: + chosen_slot = next(x for x in candidates if len(accompaniment_map[x[0]]) - len(chained.chain) >= 0) + if chosen_slot is None: + raise Exception('Something went wrong with sfx chains') + chosen_set, chosen_id = chosen_slot + chained.target_set, chained.target_id = chosen_slot + chained.target_chain = [] + for downstream in chained.chain: + next_slot = accompaniment_map[chosen_set].pop() + ds_acc = sfx_map[chained.sfx_set][downstream] + ds_acc.target_set, ds_acc.target_id = chosen_set, next_slot + chained.target_chain.append(next_slot) + candidates.remove(chosen_slot) + sfx_pool.remove(chained) + + unchained_sfx = [x for x in sfx_pool if not x.accomp] + # do the rest + for sfx in unchained_sfx: + chosen_slot = candidates.pop() + sfx.target_set, sfx.target_id = chosen_slot + + return sfx_map + + +sfx_table = { + 2: 0x1a8c29, + 3: 0x1A8D25 +} + +# 0x1a8c29 +# d8059 + +sfx_accompaniment_table = { + 2: 0x1A8CA7, + 3: 0x1A8DA3 +} + + +def randomize_sfx(rom): + sfx_map = shuffle_sfx_data() + + for shuffled_sfx in sfx_map.values(): + for sfx in shuffled_sfx.values(): + base_address = sfx_table[sfx.target_set] + rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr)) + ac_base = sfx_accompaniment_table[sfx.target_set] + last = sfx.target_id + if sfx.target_chain: + for chained in sfx.target_chain: + rom.write_byte(ac_base + last - 1, chained) + last = chained + rom.write_byte(ac_base + last - 1, 0) + + + + + + + + + diff --git a/source/classes/constants.py b/source/classes/constants.py index 04cbde2e..e03fba44 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -107,7 +107,8 @@ SETTINGSTOPROCESS = { "menuspeed": "fastmenu", "owpalettes": "ow_palettes", "uwpalettes": "uw_palettes", - "reduce_flashing": "reduce_flashing" + "reduce_flashing": "reduce_flashing", + "shuffle_sfx": "shuffle_sfx", }, "generation": { "createspoiler": "create_spoiler", diff --git a/source/gui/adjust/overview.py b/source/gui/adjust/overview.py index 4ae57e2e..7e16b1a9 100644 --- a/source/gui/adjust/overview.py +++ b/source/gui/adjust/overview.py @@ -103,6 +103,7 @@ def adjust_page(top, parent, settings): "quickswap": "quickswap", "nobgm": "disablemusic", "reduce_flashing": "reduce_flashing", + "shuffle_sfx": "shuffle_sfx", } guiargs = Namespace() for option in options: From df0218c7a199f06524f41ba94808d79568479a63 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 22 Jul 2021 16:09:28 -0700 Subject: [PATCH 009/104] last ditch swap fixes --- Fill.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index 2a578ff7..29226afd 100644 --- a/Fill.py +++ b/Fill.py @@ -273,8 +273,11 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite return 3 return 4 - possible_swaps = [x for x in state.locations_checked - if x.item.type not in ['Event', 'Crystal']] + if item_to_place.type == 'Crystal': + possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + else: + possible_swaps = [x for x in state.locations_checked + if x.item.type not in ['Event', 'Crystal'] and not x.forced_item] swap_locations = sorted(possible_swaps, key=location_preference) for location in swap_locations: @@ -283,6 +286,7 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, keys_in_itempool, single_player_placement) if new_spot: + restore_item = new_spot.item new_spot.item = item_to_place swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, keys_in_itempool, single_player_placement) @@ -292,6 +296,9 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite locations.remove(swap_spot) locations.append(new_spot) return new_spot + else: + new_spot.item = restore_item + return None From 8c1e746556495f8135d8c7315feddee487ce3a27 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 22 Jul 2021 16:26:08 -0700 Subject: [PATCH 010/104] Inverted pathing fix (Castle S&Q may be required) --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 5231fc91..264b2e5a 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1835,7 +1835,7 @@ def find_accessible_entrances(world, player, builder): elif world.mode[player] != 'inverted': start_regions = ['Links House', 'Sanctuary'] else: - start_regions = ['Inverted Links House', 'Inverted Dark Sanctuary'] + start_regions = ['Inverted Links House', 'Inverted Dark Sanctuary', 'Hyrule Castle Ledge'] regs = convert_regions(start_regions, world, player) visited_regions = set() visited_entrances = [] From 46bfcb4dde7b1d8cb3fa9e23530a2f538d6d0f9f Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 23 Jul 2021 08:39:31 -0700 Subject: [PATCH 011/104] Aga Tower shouldn't choose inverted links house if not shuffled --- EntranceShuffle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e111ddff..cd8df010 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1446,7 +1446,7 @@ def link_inverted_entrances(world, player): # shuffle aga door first. if it's on hc ledge, then one other hc ledge door has to be must_exit all_entrances_aga = lw_entrances + dw_entrances - aga_doors = [i for i in all_entrances_aga] + aga_doors = [i for i in all_entrances_aga if world.shufflelinks[player] or i != 'Inverted Links House'] random.shuffle(aga_doors) aga_door = aga_doors.pop() @@ -1589,8 +1589,9 @@ def link_inverted_entrances(world, player): hc_ledge_entrances = ['Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Inverted Ganons Tower'] # shuffle aga door. if it's on hc ledge, then one other hc ledge door has to be must_exit - aga_door = random.choice(entrances) - + aga_choices = [x for x in entrances if world.shufflelinks[player] or x != 'Inverted Links House'] + aga_door = random.choice(aga_choices) + if aga_door in hc_ledge_entrances: hc_ledge_entrances.remove(aga_door) From 8ed6b3fad27752955dbe6edfabf4a49866099408 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 23 Jul 2021 12:37:41 -0700 Subject: [PATCH 012/104] Exception for DP back on basic,intensity level 1 (simply can't make a passage) --- DoorShuffle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DoorShuffle.py b/DoorShuffle.py index 264b2e5a..02f54069 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -502,6 +502,9 @@ def analyze_portals(world, player): raise Exception('please inspect this case') if len(reachable_portals) == 1: info.sole_entrance = reachable_portals[0] + if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace': + if len(inaccessible_portals) == 1 and inaccessible_portals[0] == 'Desert Back': + info.required_passage.clear() # can't make a passage at this intensity level, something else must exit info_map[dungeon] = info for dungeon, info in info_map.items(): From 65c583c082cf7951eb3113fd47230b9021392938 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 27 Jul 2021 16:00:05 -0600 Subject: [PATCH 013/104] Initial work on dungeon prize logic in key layouts --- DungeonGenerator.py | 10 ++++ Dungeons.py | 16 ++++++ KeyDoorShuffle.py | 137 +++++++++++++++++++++++++++++++++----------- 3 files changed, 129 insertions(+), 34 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 9d76ddb5..86be2a04 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -807,6 +807,10 @@ class ExplorationState(object): self.dungeon = dungeon self.pinball_used = False + self.prize_door_set = {} + self.prize_doors = [] + self.prize_doors_opened = False + def copy(self): ret = ExplorationState(dungeon=self.dungeon) ret.unattached_doors = list(self.unattached_doors) @@ -833,6 +837,10 @@ class ExplorationState(object): ret.non_door_entrances = list(self.non_door_entrances) ret.dungeon = self.dungeon ret.pinball_used = self.pinball_used + + ret.prize_door_set = dict(self.prize_door_set) + ret.prize_doors = list(self.prize_doors) + ret.prize_doors_opened = self.prize_doors_opened return ret def next_avail_door(self): @@ -868,6 +876,8 @@ class ExplorationState(object): if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) + if '- Prize' in location.name: + self.prize_received = True def flooded_key_check(self, location): if location.name not in flooded_keys.keys(): diff --git a/Dungeons.py b/Dungeons.py index 6f0f2197..eca190c1 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -392,6 +392,22 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } +dungeon_prize = { + 'Hyrule Castle': None, + 'Eastern Palace': 'Eastern Palace - Prize', + 'Desert Palace': 'Desert Palace - Prize', + 'Tower of Hera': 'Tower of Hera - Prize', + 'Agahnims Tower': None, + 'Palace of Darkness': 'Palace of Darkness - Prize', + 'Swamp Palace': 'Swamp Palace - Prize', + 'Skull Woods': 'Skull Woods - Prize', + 'Thieves Town': 'Thieves Town - Prize', + 'Ice Palace': 'Ice Palace - Prize', + 'Misery Mire': 'Misery Mire - Prize', + 'Turtle Rock': 'Turtle Rock - Prize', + 'Ganons Tower': None +} + dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', 'Eastern Palace': 'in Eastern Palace', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 89fa307d..0ecb98e4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -2,9 +2,9 @@ import itertools import logging from collections import defaultdict, deque -from BaseClasses import DoorType, dungeon_keys, KeyRuleType +from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_bigs +from Dungeons import dungeon_bigs, dungeon_prize from DungeonGenerator import ExplorationState, special_big_key_doors @@ -26,6 +26,7 @@ class KeyLayout(object): self.item_locations = set() self.found_doors = set() + self.prize_relevant = False # bk special? # bk required? True if big chests or big doors exists @@ -36,6 +37,7 @@ class KeyLayout(object): self.max_chests = calc_max_chests(builder, self, world, player) self.all_locations = set() self.item_locations = set() + self.prize_relevant = False class KeyLogic(object): @@ -183,6 +185,8 @@ class KeyCounter(object): self.important_location = False self.other_locations = {} self.important_locations = {} + self.prize_doors_opened = False + self.prize_received = False def used_smalls_loc(self, reserve=0): return max(self.used_keys + reserve - len(self.key_only_locations), 0) @@ -242,7 +246,7 @@ def analyze_dungeon(key_layout, world, player): if world.retro[player] and world.mode[player] != 'standard': return - original_key_counter = find_counter({}, False, key_layout) + original_key_counter = find_counter({}, False, key_layout, False) queue = deque([(None, original_key_counter)]) doors_completed = set() visited_cid = set() @@ -270,15 +274,18 @@ def analyze_dungeon(key_layout, world, player): child_queue.append((child, odd_counter, empty_flag)) while len(child_queue) > 0: child, odd_counter, empty_flag = child_queue.popleft() - if not child.bigKey and child not in doors_completed: + prize_flag = key_counter.prize_doors_opened + if child in key_layout.flat_prop and child not in doors_completed: best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag) rule = create_rule(best_counter, key_counter, world, player) create_worst_case_rule(rule, best_counter, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) key_logic.door_rules[child.name] = rule + elif not child.bigKey and child not in doors_completed: + prize_flag = True doors_completed.add(child) - next_counter = find_next_counter(child, key_counter, key_layout) + next_counter = find_next_counter(child, key_counter, key_layout, prize_flag) ctr_id = cid(next_counter, key_layout) if ctr_id not in visited_cid: queue.append((child, next_counter)) @@ -300,6 +307,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic max_ctr = find_max_counter(key_layout) for code, key_counter in key_layout.key_counters.items(): + if key_counter.prize_received and not key_counter.prize_doors_opened: + continue # we have the prize, we are not concerned about this case accessible_loc = set() accessible_loc.update(key_counter.free_locations) accessible_loc.update(key_counter.key_only_locations) @@ -684,7 +693,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # for new_door in door_set: proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened) bk_open = new_counter.big_key_opened if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter): ignored_doors.add(new_door) @@ -736,7 +745,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, for new_child in new_children: proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])} bk_open = bk_opened or new_door.bigKey - new_counter = find_counter(proposed_doors, bk_open, key_layout) + new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened) if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player): wasted_keys += 1 if new_avail - wasted_keys < old_avail: @@ -744,10 +753,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, return False -def find_next_counter(new_door, old_counter, key_layout): +def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} bk_open = old_counter.big_key_opened or new_door.bigKey - return find_counter(proposed_doors, bk_open, key_layout) + prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened + return find_counter(proposed_doors, bk_open, key_layout, prize_flag) def check_special_locations(locations): @@ -835,7 +845,7 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, True) changed = True return counter @@ -856,7 +866,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors): bk_hint = counter.big_key_opened for d in doors_to_open.keys(): bk_hint = bk_hint or d.bigKey - counter = find_counter(proposed_doors, bk_hint, key_layout) + counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened) changed = True return counter @@ -939,12 +949,19 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c return worst_counter, post_counter, bk_rule_num -def open_a_door(door, child_state, flat_proposal): +def open_a_door(door, child_state, flat_proposal, world, player): if door.bigKey or door.name in special_big_key_doors: child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) child_state.big_doors.clear() + elif door in child_state.prize_door_set: + child_state.prize_doors_opened = True + for exp_door in child_state.prize_doors: + new_region = exp_door.door.entrance.parent_region + child_state.visit_region(new_region, key_checks=True) + child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player) + child_state.prize_doors.clear() else: child_state.opened_doors.append(door) doors_to_open = [x for x in child_state.small_doors if x.door == door] @@ -1002,7 +1019,7 @@ def count_unique_small_doors(key_counter, proposal): def exist_relevant_big_doors(key_counter, key_layout): - bk_counter = find_counter(key_counter.open_doors, True, key_layout, False) + bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False) if bk_counter is not None: diff = dict_difference(bk_counter.free_locations, key_counter.free_locations) if len(diff) > 0: @@ -1338,8 +1355,13 @@ def validate_key_layout(key_layout, world, player): state.key_locations = key_layout.max_chests state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player) @@ -1370,7 +1392,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if smalls_avail and available_small_locations > 0: for exp_door in state.small_doors: state_copy = state.copy() - open_a_door(exp_door.door, state_copy, flat_proposal) + open_a_door(exp_door.door, state_copy, flat_proposal, world, player) state_copy.used_smalls += 1 if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 @@ -1385,7 +1407,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa return False if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)): state_copy = state.copy() - open_a_door(state.big_doors[0].door, state_copy, flat_proposal) + open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player) if not found_forced_bk: state_copy.used_locations += 1 code = state_id(state_copy, flat_proposal) @@ -1397,6 +1419,18 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa valid = checked_states[code] if not valid: return False + if not state.prize_doors_opened and key_layout.prize_relevant: + state_copy = state.copy() + open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) + code = state_id(state_copy, flat_proposal) + if code not in checked_states.keys(): + valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, + state, available_small_locations, world, player) + checked_states[code] = valid + else: + valid = checked_states[code] + if not valid: + return False return True @@ -1407,7 +1441,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p state_copy = state.copy() while len(new_bk_doors) > 0: for door in new_bk_doors: - open_a_door(door.door, state_copy, key_layout.flat_prop) + open_a_door(door.door, state_copy, key_layout.flat_prop, world, player) new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors)) expand_key_state(state_copy, key_layout.flat_prop, world, player) new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations)) @@ -1461,8 +1495,14 @@ def create_key_counters(key_layout, world, player): state.big_key_special = True special_region = region for region in key_layout.start_regions: - state.visit_region(region, key_checks=True) - state.add_all_doors_check_keys(region, flat_proposal, world, player) + dungeon_entrance, portal_door = find_outside_connection(region) + if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + state.append_door_to_list(portal_door, state.prize_doors) + state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True + else: + state.visit_region(region, key_checks=True) + state.add_all_doors_check_keys(region, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) code = state_id(state, key_layout.flat_prop) key_counters[code] = create_key_counter(state, key_layout, world, player) @@ -1478,7 +1518,7 @@ def create_key_counters(key_layout, world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): - open_a_door(door, child_state, flat_proposal) + open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) if code not in key_counters.keys(): @@ -1488,9 +1528,19 @@ def create_key_counters(key_layout, world, player): return key_counters +def find_outside_connection(region): + portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None) + if portal: + dungeon_entrance = next(x for x in portal.parent_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region) + return dungeon_entrance, portal_entrance.door + return None, None + + def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) - key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors))) + key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors))) for loc in state.found_locations: if important_location(loc, world, player): key_counter.important_location = True @@ -1507,6 +1557,10 @@ def create_key_counter(state, key_layout, world, player): key_counter.open_doors.update(dict.fromkeys(state.opened_doors)) key_counter.used_keys = count_unique_sm_doors(state.opened_doors) key_counter.big_key_opened = state.big_key_opened + if len(state.prize_door_set) > 0 and state.prize_doors_opened: + key_counter.prize_doors_opened = True + if any(x for x in key_counter.important_locations if '- Prize' in x.name): + key_counter.prize_received = True return key_counter @@ -1557,11 +1611,13 @@ def state_id(state, flat_proposal): s_id = '1' if state.big_key_opened else '0' for d in flat_proposal: s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' return s_id -def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): - counter = find_counter_hint(opened_doors, bk_hint, key_layout) +def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True): + counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag) if counter is not None: return counter more_doors = [] @@ -1570,43 +1626,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True): if door.dest not in opened_doors.keys(): more_doors.append(door.dest) if len(more_doors) > len(opened_doors.keys()): - counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout) + counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag) if counter is not None: return counter if raise_on_error: - raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop)) + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) + raise Exception(f'Unable to find door permutation. Init CID: {cid}') return None -def find_counter_hint(opened_doors, bk_hint, key_layout): - cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop) +def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag): + cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] if not bk_hint: - cid = counter_id(opened_doors, True, key_layout.flat_prop) + cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag) if cid in key_layout.key_counters.keys(): return key_layout.key_counters[cid] return None def find_max_counter(key_layout): - max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True) if max_counter is None: raise Exception("Max Counter is none - something is amiss") if len(max_counter.child_doors) > 0: - max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout) + max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True) return max_counter -def counter_id(opened_doors, bk_unlocked, flat_proposal): +def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag): s_id = '1' if bk_unlocked else '0' for d in flat_proposal: s_id += '1' if d in opened_doors.keys() else '0' + if prize_relevant: + s_id += '1' if prize_flag else '0' return s_id def cid(counter, key_layout): - return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop) + return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop, + key_layout.prize_relevant, counter.prize_doors_opened) # class SoftLockException(Exception): @@ -1816,8 +1876,17 @@ def validate_key_placement(key_layout, world, player): found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name) found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \ len(counter.key_only_locations) + keys_outside + if key_layout.prize_relevant: + found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) + if not found_prize: + prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + # todo: pyramid fairy only care about crystals 5 & 6 + found_prize = 'Crystal' not in prize_loc.item.name + else: + found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ - found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) + found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \ + (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize) if not can_progress: missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name] From 4c15c193f42e6156e66e5558ef8daf68210754e3 Mon Sep 17 00:00:00 2001 From: StructuralMike <66819228+StructuralMike@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:27:49 +0200 Subject: [PATCH 014/104] Set bomb upgrades as advancement before beemizer Beemizer can replace bomb upgrades in bomblogic unless they're tagged as advancement earlier --- ItemList.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ItemList.py b/ItemList.py index 57566418..9128436a 100644 --- a/ItemList.py +++ b/ItemList.py @@ -301,6 +301,11 @@ def generate_itempool(world, player): world.get_location(location, player).event = True world.get_location(location, player).locked = True + if world.bomblogic[player]: + for item in world.itempool: + if item.name == 'Bomb Upgrade (+10)' and item.player == player: + item.advancement = True + if world.shopsanity[player]: for shop in world.shops[player]: if shop.region.name in shop_to_location_table: @@ -524,9 +529,6 @@ def set_up_shops(world, player): cap_shop = world.get_region('Capacity Upgrade', player).shop cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro if world.bomblogic[player]: - for item in world.itempool: - if item.name == 'Bomb Upgrade (+10)' and item.player == player: - item.advancement = True if world.shopsanity[player]: removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] for remove in removals: From c06477c0f20fb00fdbd80b893e10ffb7b6a3ad42 Mon Sep 17 00:00:00 2001 From: StructuralMike <66819228+StructuralMike@users.noreply.github.com> Date: Thu, 29 Jul 2021 15:51:56 +0200 Subject: [PATCH 015/104] Update ItemList.py --- ItemList.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ItemList.py b/ItemList.py index 9128436a..e79392b8 100644 --- a/ItemList.py +++ b/ItemList.py @@ -301,11 +301,6 @@ def generate_itempool(world, player): world.get_location(location, player).event = True world.get_location(location, player).locked = True - if world.bomblogic[player]: - for item in world.itempool: - if item.name == 'Bomb Upgrade (+10)' and item.player == player: - item.advancement = True - if world.shopsanity[player]: for shop in world.shops[player]: if shop.region.name in shop_to_location_table: @@ -323,6 +318,11 @@ def generate_itempool(world, player): p_item = next(item for item in items if item.name == potion and item.player == player) p_item.priority = True # don't beemize one of each potion + if world.bomblogic[player]: + for item in items: + if item.name == 'Bomb Upgrade (+10)' and item.player == player: + item.advancement = True + world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms if clock_mode is not None: From 8a361e9672dee24e20b7e3d2dfe0f5c2e4d633a2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 2 Aug 2021 12:39:48 -0600 Subject: [PATCH 016/104] Various refinements and fixes to key logic and generation --- DoorShuffle.py | 4 ++++ DungeonGenerator.py | 6 +++--- Dungeons.py | 3 --- Fill.py | 4 ++-- KeyDoorShuffle.py | 31 ++++++++++++++++++++++++------- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 02f54069..89e7a6d6 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1823,6 +1823,10 @@ def find_inaccessible_regions(world, player): if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'): queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) + if world.mode[player] == 'inverted': + ledge = world.get_region('Hyrule Castle Ledge', 1) + if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): + world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') logger.debug('Inaccessible Regions:') for r in world.inaccessible_regions[player]: diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 86be2a04..47922be5 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1113,9 +1113,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player): def valid_region_to_explore(region, name, world, player): if region is None: return False - return (region.type == RegionType.Dungeon and region.dungeon.name in name)\ - or region.name in world.inaccessible_regions[player]\ - or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard') + return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name) + or region.name in world.inaccessible_regions[player] + or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')) def get_doors(world, region, player): diff --git a/Dungeons.py b/Dungeons.py index eca190c1..e3b76c28 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -393,11 +393,9 @@ dungeon_bigs = { } dungeon_prize = { - 'Hyrule Castle': None, 'Eastern Palace': 'Eastern Palace - Prize', 'Desert Palace': 'Desert Palace - Prize', 'Tower of Hera': 'Tower of Hera - Prize', - 'Agahnims Tower': None, 'Palace of Darkness': 'Palace of Darkness - Prize', 'Swamp Palace': 'Swamp Palace - Prize', 'Skull Woods': 'Skull Woods - Prize', @@ -405,7 +403,6 @@ dungeon_prize = { 'Ice Palace': 'Ice Palace - Prize', 'Misery Mire': 'Misery Mire - Prize', 'Turtle Rock': 'Turtle Rock - Prize', - 'Ganons Tower': None } dungeon_hints = { diff --git a/Fill.py b/Fill.py index 29226afd..d75cf080 100644 --- a/Fill.py +++ b/Fill.py @@ -722,7 +722,7 @@ def balance_money_progression(world): if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]: wallet[player] += income rooms_visited[player].add(room) - if checked_locations: + if checked_locations or len(unchecked_locations) == 0: if world.has_beaten_game(state): done = True continue @@ -732,7 +732,7 @@ def balance_money_progression(world): solvent = set() insolvent = set() for player in range(1, world.players+1): - if wallet[player] >= sphere_costs[player] > 0: + if wallet[player] >= sphere_costs[player] >= 0: solvent.add(player) if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: insolvent.add(player) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 0ecb98e4..4c591304 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -307,7 +307,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): key_logic = key_layout.key_logic max_ctr = find_max_counter(key_layout) for code, key_counter in key_layout.key_counters.items(): - if key_counter.prize_received and not key_counter.prize_doors_opened: + if skip_key_counter_due_to_prize(key_layout, key_counter): continue # we have the prize, we are not concerned about this case accessible_loc = set() accessible_loc.update(key_counter.free_locations) @@ -343,6 +343,10 @@ def create_exhaustive_placement_rules(key_layout, world, player): refine_location_rules(key_layout) +def skip_key_counter_due_to_prize(key_layout, key_counter): + return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened + + def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player): if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations)) @@ -1356,7 +1360,8 @@ def validate_key_layout(key_layout, world, player): state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + if (key_layout.prize_relevant and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance else: @@ -1396,7 +1401,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa state_copy.used_smalls += 1 if state_copy.used_smalls > ttl_small_key_only: state_copy.used_locations += 1 - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1410,7 +1415,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player) if not found_forced_bk: state_copy.used_locations += 1 - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1422,7 +1427,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not state.prize_doors_opened and key_layout.prize_relevant: state_copy = state.copy() open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) - code = state_id(state_copy, flat_proposal) + code = validate_id(state_copy, flat_proposal) if code not in checked_states.keys(): valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal, state, available_small_locations, world, player) @@ -1496,7 +1501,9 @@ def create_key_counters(key_layout, world, player): special_region = region for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']: + if (len(key_layout.start_regions) > 1 and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] + and key_layout.key_logic.dungeon in dungeon_prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1616,6 +1623,16 @@ def state_id(state, flat_proposal): return s_id +def validate_id(state, flat_proposal): + s_id = '1' if state.big_key_opened else '0' + for d in flat_proposal: + s_id += '1' if d in state.opened_doors else '0' + if len(state.prize_door_set) > 0: + s_id += '1' if state.prize_doors_opened else '0' + s_id += str(state.used_locations) + return s_id + + def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True): counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag) if counter is not None: @@ -1878,7 +1895,7 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize: + if not found_prize and key_layout.sector.name in dungeon_prize: prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name From 50296915a718a9b629c225368451bbfe9c1e3e46 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 2 Aug 2021 12:40:29 -0600 Subject: [PATCH 017/104] Uncertain key rule change (keeping my eye on it) --- KeyDoorShuffle.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4c591304..e7bcae11 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -61,7 +61,7 @@ class KeyLogic(object): def check_placement(self, unplaced_keys, big_key_loc=None): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, unplaced_keys): + if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -149,13 +149,20 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys): + def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc): bk_blocked = False if self.bk_conditional_set: for loc in self.bk_conditional_set: if loc.item and loc.item.bigkey: bk_blocked = True break + else: + def loc_has_bk(l): + return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) + + bk_found = any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) + if not bk_found: + return True check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk if not bk_blocked and check_locations is None: return True From bcaee735a2835ca2d3ef3b99a24d29ae9b40be2c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 2 Aug 2021 15:24:13 -0600 Subject: [PATCH 018/104] SFX shuffle added --- Main.py | 2 +- RELEASENOTES.md | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index e44c804e..df4426e8 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.0.1-u' +__version__ = '0.5.0.2-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c0fae696..b6be375d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,12 +1,22 @@ # New Features -Bomb Logic added as an option. This removes your ability to use bombs until you find a "bomb bag", a +10 Bomb Capacity item. It is accounted for in the logic, so you aren't expected to get items behind bomb walls until you have found the bomb capacity item. The upgrades are removed from the upgrade fairy as well. +## Shuffle SFX + +Shuffles a large portion of the sounds effects. Can be used with the adjuster. + +CLI: ```--shuffle_sfx``` -``` ---bomblogic -``` +## Bomb Logic + +When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable. + +CLI: ```--bomblogic``` + # Bug Fixes and Notes. + +* 0.5.0.2 + * --shuffle_sfx option added * 0.5.0.1 * --bomblogic option added * 0.5.0.0 From 229747b4a417271bf5b599bee3ee653e59bb9a4d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 3 Aug 2021 09:29:16 -0500 Subject: [PATCH 019/104] Added dynamic flute exits --- Main.py | 4 +++- OverworldShuffle.py | 18 ++++++++++++++---- Regions.py | 2 +- Rules.py | 2 -- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Main.py b/Main.py index 007595de..6bf42916 100644 --- a/Main.py +++ b/Main.py @@ -16,7 +16,7 @@ from OverworldGlitchRules import create_owg_connections from PotShuffle import shuffle_pots from Regions import create_regions, create_shops, mark_light_world_regions, mark_dark_world_regions, create_dungeon_regions, adjust_locations from OWEdges import create_owedges -from OverworldShuffle import link_overworld +from OverworldShuffle import link_overworld, create_flute_exits from EntranceShuffle import link_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors @@ -162,6 +162,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_overworld(world, player) + create_flute_exits(world, player) logger.info(world.fish.translate("cli","cli","shuffling.world")) @@ -420,6 +421,7 @@ def copy_world(world): for player in range(1, world.players + 1): create_regions(ret, player) + create_flute_exits(ret, player) create_dungeon_regions(ret, player) create_shops(ret, player) create_rooms(ret, player) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 612a5f7e..f2a0bb4a 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -1,5 +1,5 @@ import RaceRandom as random, logging, copy -from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot +from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel __version__ = '0.1.7.2-u' @@ -500,6 +500,18 @@ def reorganize_groups(world, groups, player): else: raise NotImplementedError('Shuffling not supported yet') +def create_flute_exits(world, player): + for region in world.regions: + if ((region.type == RegionType.LightWorld and region.name not in world.owswaps[player][1]) + or (region.type == RegionType.DarkWorld and region.name in world.owswaps[player][2])) \ + and region.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']: + exitname = 'Flute From ' + region.name + exit = Entrance(region.player, exitname, region) + exit.access_rule = lambda state: state.can_flute(player) + exit.connect(world.get_region('Flute Sky', player)) + region.exits.append(exit) + world.initialize_regions() + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') @@ -513,9 +525,7 @@ temporary_mandatory_connections = [ ] # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions -mandatory_connections = [('Flute Away', 'Flute Sky'), - - # Whirlpool Connections +mandatory_connections = [# Whirlpool Connections ('C Whirlpool', 'River Bend Water'), ('River Bend Whirlpool', 'C Whirlpool Water'), ('Lake Hylia Whirlpool', 'Zora Waterfall Water'), diff --git a/Regions.py b/Regions.py index 40d84a28..c916716e 100644 --- a/Regions.py +++ b/Regions.py @@ -79,7 +79,7 @@ def create_regions(world, player): create_lw_region(player, 'Flute Boy Area', ['Flute Spot'], ['Stumpy Mirror Spot', 'Flute Boy SC']), create_lw_region(player, 'Flute Boy Pass', None, ['Stumpy Pass Mirror Spot', 'Flute Boy WS', 'Flute Boy SW']), create_lw_region(player, 'Central Bonk Rocks Area', None, ['Bonk Fairy (Light)', 'Dark Bonk Rocks Mirror Spot', 'Central Bonk Rocks NW', 'Central Bonk Rocks SW', 'Central Bonk Rocks EN', 'Central Bonk Rocks EC', 'Central Bonk Rocks ES']), - create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES', 'Flute Away']), + create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES']), create_lw_region(player, 'Stone Bridge Area', None, ['Hammer Bridge North Mirror Spot', 'Hammer Bridge South Mirror Spot', 'Stone Bridge NC', 'Stone Bridge EN', 'Stone Bridge WS', 'Stone Bridge SC']), create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC']), create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC']), diff --git a/Rules.py b/Rules.py index 0a1bc261..ba46fe55 100644 --- a/Rules.py +++ b/Rules.py @@ -792,8 +792,6 @@ def default_rules(world, player): set_rule(world.get_entrance('Dark C Whirlpool Rock (Bottom)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Dark C Whirlpool Rock (Top)', player), lambda state: state.can_lift_rocks(player)) - set_rule(world.get_entrance('Flute Away', player), lambda state: state.can_flute(player)) - set_rule(world.get_entrance('Zora Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Zora Waterfall Water Entry', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Waterfall of Wishing Cave Entry', player), lambda state: state.has('Flippers', player)) From aeb910e2746bac2c4b950398e5da68afd10e2949 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 08:43:47 -0600 Subject: [PATCH 020/104] Various fixes: Full boss shuffle samples without replacement Vanilla doors simply unmarks ugly smalls Money balance: handle multiworld solvent players better Perf for item pool adjustment Add loglevel to Mystery --- Bosses.py | 4 ++-- DoorShuffle.py | 9 +++++++++ Fill.py | 2 +- ItemList.py | 8 ++++---- Mystery.py | 2 ++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Bosses.py b/Bosses.py index 253717bd..2718431e 100644 --- a/Bosses.py +++ b/Bosses.py @@ -176,8 +176,8 @@ def place_bosses(world, player): if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] - else: # all bosses present, the three duplicates chosen at random - bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)] + else: # all bosses present, the three duplicates chosen at random + bosses = all_bosses + random.sample(placeable_bosses, 3) logging.getLogger('').debug('Bosses chosen %s', bosses) diff --git a/DoorShuffle.py b/DoorShuffle.py index d5b69126..ca4b9139 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -101,6 +101,8 @@ def link_doors_main(world, player): connect_portal(portal, world, player) if not world.doorShuffle[player] == 'vanilla': fix_big_key_doors_with_ugly_smalls(world, player) + else: + unmark_ugly_smalls(world, player) if world.doorShuffle[player] == 'vanilla': for entrance, ext in open_edges: connect_two_way(world, entrance, ext, player) @@ -316,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player): y.dest = x +def unmark_ugly_smalls(world, player): + for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S', + 'TR Lava Escape SE', 'GT Hidden Spikes SE']: + door = world.get_door(d, player) + door.smallKey = False + + def fix_big_key_doors_with_ugly_smalls(world, player): remove_ugly_small_key_doors(world, player) unpair_big_key_doors(world, player) diff --git a/Fill.py b/Fill.py index a6ef6cf4..04278d43 100644 --- a/Fill.py +++ b/Fill.py @@ -736,7 +736,7 @@ def balance_money_progression(world): solvent.add(player) if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: insolvent.add(player) - if len(solvent) == 0: + if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) difference = sphere_costs[target_player]-wallet[target_player] logger.debug(f'Money balancing needed: Player {target_player} short {difference}') diff --git a/ItemList.py b/ItemList.py index e79392b8..eaa78365 100644 --- a/ItemList.py +++ b/ItemList.py @@ -347,11 +347,11 @@ def generate_itempool(world, player): # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0): - [item for item in items if item.name == 'Boss Heart Container'][0].advancement = True + next(item for item in items if item.name == 'Boss Heart Container').advancement = True elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4): - adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4] - for hp in adv_heart_pieces: - hp.advancement = True + adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart') + for i in range(4): + next(adv_heart_pieces).advancement = True beeweights = {'0': {None: 100}, '1': {None: 75, 'trap': 25}, diff --git a/Mystery.py b/Mystery.py index 306b5c27..3c62e72a 100644 --- a/Mystery.py +++ b/Mystery.py @@ -31,6 +31,7 @@ def main(): parser.add_argument('--rom') parser.add_argument('--enemizercli') parser.add_argument('--outputpath') + parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) for player in range(1, multiargs.multi + 1): parser.add_argument(f'--p{player}', help=argparse.SUPPRESS) args = parser.parse_args() @@ -63,6 +64,7 @@ def main(): erargs.race = True erargs.outputname = seedname erargs.outputpath = args.outputpath + erargs.loglevel = args.loglevel if args.rom: erargs.rom = args.rom From 24b01cfcc7c48639c48ecd76a2a8290945a21fc6 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 09:26:41 -0600 Subject: [PATCH 021/104] Performance optimizations --- BaseClasses.py | 4 +++ EntranceShuffle.py | 7 ++-- Fill.py | 79 +++++++++++++++++++++++++++------------------- Main.py | 17 +++++----- Rom.py | 3 +- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b9f33267..087bda1b 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -434,8 +434,12 @@ class World(object): def can_beat_game(self, starting_state=None): if starting_state: + if self.has_beaten_game(starting_state): + return True state = starting_state.copy() else: + if self.has_beaten_game(self.state): + return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 1c1a6ab8..7e5dcdd3 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2120,10 +2120,11 @@ def connect_doors(world, doors, targets, player): """This works inplace""" random.shuffle(doors) random.shuffle(targets) - while doors: - door = doors.pop() - target = targets.pop() + placing = min(len(doors), len(targets)) + for door, target in zip(doors, targets): connect_entrance(world, door, target, player) + doors[:] = doors[placing:] + targets[:] = targets[placing:] def skull_woods_shuffle(world, player): diff --git a/Fill.py b/Fill.py index 04278d43..d1905daf 100644 --- a/Fill.py +++ b/Fill.py @@ -1,4 +1,6 @@ import RaceRandom as random +import collections +import itertools import logging from BaseClasses import CollectionState @@ -491,9 +493,8 @@ def sell_keys(world, player): def balance_multiworld_progression(world): state = CollectionState(world) - checked_locations = [] - unchecked_locations = world.get_locations().copy() - random.shuffle(unchecked_locations) + checked_locations = set() + unchecked_locations = set(world.get_locations()) reachable_locations_count = {} for player in range(1, world.players + 1): @@ -501,7 +502,7 @@ def balance_multiworld_progression(world): def get_sphere_locations(sphere_state, locations): sphere_state.sweep_for_events(key_only=True, locations=locations) - return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] + return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)} while True: sphere_locations = get_sphere_locations(state, unchecked_locations) @@ -512,38 +513,42 @@ def balance_multiworld_progression(world): if checked_locations: threshold = max(reachable_locations_count.values()) - 20 - balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] - if balancing_players is not None and len(balancing_players) > 0: + balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold} + if balancing_players: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() balancing_sphere = sphere_locations.copy() - candidate_items = [] + candidate_items = collections.defaultdict(set) while True: for location in balancing_sphere: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): balancing_state.collect(location.item, True, location) - if location.item.player in balancing_players and not location.locked: - candidate_items.append(location) + player = location.item.player + if player in balancing_players and not location.locked and location.player != player: + candidate_items[player].add(location) balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations) for location in balancing_sphere: balancing_unchecked_locations.remove(location) balancing_reachables[location.player] += 1 - if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]): + if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()): break elif not balancing_sphere: raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') - unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations] + unlocked_locations = collections.defaultdict(set) + for l in unchecked_locations: + if l not in balancing_unchecked_locations: + unlocked_locations[l.player].add(l) items_to_replace = [] for player in balancing_players: - locations_to_test = [l for l in unlocked_locations if l.player == player] - # only replace items that end up in another player's world - items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player] + locations_to_test = unlocked_locations[player] + items_to_test = candidate_items[player] while items_to_test: testing = items_to_test.pop() reducing_state = state.copy() - for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]: + for location in itertools.chain((l for l in items_to_replace if l.item.player == player), + items_to_test): reducing_state.collect(location.item, True, location) reducing_state.sweep_for_events(locations=locations_to_test) @@ -557,33 +562,43 @@ def balance_multiworld_progression(world): items_to_replace.append(testing) replaced_items = False - replacement_locations = [l for l in checked_locations if not l.event and not l.locked] + # sort then shuffle to maintain deterministic behaviour, + # while allowing use of set for better algorithm growth behaviour elsewhere + replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) + world.random.shuffle(replacement_locations) + items_to_replace.sort() + world.random.shuffle(items_to_replace) while replacement_locations and items_to_replace: - new_location = replacement_locations.pop() old_location = items_to_replace.pop() + for new_location in replacement_locations: + if (new_location.can_fill(state, old_location.item, False) and + old_location.can_fill(state, new_location.item, False)): + replacement_locations.remove(new_location) + new_location.item, old_location.item = old_location.item, new_location.item + if world.shopsanity[new_location.player]: + check_shop_swap(new_location) + if world.shopsanity[old_location.player]: + check_shop_swap(old_location) + new_location.event, old_location.event = True, False + logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, " + f"displacing {old_location.item} into {old_location}") + state.collect(new_location.item, True, new_location) + replaced_items = True + break + else: + logging.warning(f"Could not Progression Balance {old_location.item}") - while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)): - replacement_locations.insert(0, new_location) - new_location = replacement_locations.pop() - - new_location.item, old_location.item = old_location.item, new_location.item - if world.shopsanity[new_location.player]: - check_shop_swap(new_location) - if world.shopsanity[old_location.player]: - check_shop_swap(old_location) - new_location.event, old_location.event = True, False - state.collect(new_location.item, True, new_location) - replaced_items = True if replaced_items: - for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]): + unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]} + for location in get_sphere_locations(state, unlocked): unchecked_locations.remove(location) reachable_locations_count[location.player] += 1 - sphere_locations.append(location) + sphere_locations.add(location) for location in sphere_locations: if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey): state.collect(location.item, True, location) - checked_locations.extend(sphere_locations) + checked_locations |= sphere_locations if world.has_beaten_game(state): break diff --git a/Main.py b/Main.py index b43d099a..719d0cc5 100644 --- a/Main.py +++ b/Main.py @@ -541,11 +541,11 @@ def create_playthrough(world): while sphere_candidates: state.sweep_for_events(key_only=True) - sphere = [] + sphere = set() # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres for location in sphere_candidates: if state.can_reach(location) and state.not_flooding_a_key(world, location): - sphere.append(location) + sphere.add(location) for location in sphere: sphere_candidates.remove(location) @@ -566,7 +566,7 @@ def create_playthrough(world): # in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it for num, sphere in reversed(list(enumerate(collection_spheres))): - to_delete = [] + to_delete = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player) @@ -576,15 +576,14 @@ def create_playthrough(world): world.clear_exp_cache() if world.can_beat_game(state_cache[num]): # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') - to_delete.append(location) + to_delete.add(location) else: # still required, got to keep it around # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') location.item = old_item # cull entries in spheres for spoiler walkthrough at end - for location in to_delete: - sphere.remove(location) + sphere -= to_delete # second phase, sphere 0 for item in [i for i in world.precollected_items if i.advancement]: @@ -600,7 +599,7 @@ def create_playthrough(world): # used to access it was deemed not required.) So we need to do one final sphere collection pass # to build up the correct spheres - required_locations = [item for sphere in collection_spheres for item in sphere] + required_locations = {item for sphere in collection_spheres for item in sphere} state = CollectionState(world) collection_spheres = [] while required_locations: @@ -637,7 +636,7 @@ def create_playthrough(world): old_world.spoiler.paths = dict() for player in range(1, world.players + 1): old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player}) - for _, path in dict(old_world.spoiler.paths).items(): + for path in dict(old_world.spoiler.paths).values(): if any(exit == 'Pyramid Fairy' for (_, exit) in path): if world.mode[player] != 'inverted': old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player)) @@ -645,6 +644,6 @@ def create_playthrough(world): old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])]) + old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]} for i, sphere in enumerate(collection_spheres): old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} diff --git a/Rom.py b/Rom.py index 8d86a16e..564126fb 100644 --- a/Rom.py +++ b/Rom.py @@ -102,8 +102,7 @@ class LocalRom(object): self.buffer[address] = value def write_bytes(self, startaddress, values): - for i, value in enumerate(values): - self.write_byte(startaddress + i, value) + self.buffer[startaddress:startaddress + len(values)] = values def write_to_file(self, file): with open(file, 'wb') as outfile: From e10c765bc28a91b031920bddfe410e003550cfea Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 12:16:29 -0600 Subject: [PATCH 022/104] Key logic --- KeyDoorShuffle.py | 69 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index e7bcae11..7d550d1b 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -275,7 +275,7 @@ def analyze_dungeon(key_layout, world, player): # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): - if not child.bigKey or not key_layout.big_key_special or big_avail: + if can_open_door_by_counter(child, key_counter, key_layout, world, player): odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player) empty_flag = empty_counter(odd_counter) child_queue.append((child, odd_counter, empty_flag)) @@ -460,7 +460,8 @@ def refine_placement_rules(key_layout, max_ctr): rule_b = temp if rule_a.bk_conditional_set and rule_b.check_locations_w_bk: common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk) - if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed: + common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) + if (common_needed - common_locs) * 2 > key_layout.max_chests: key_logic.bk_restricted.update(rule_a.bk_conditional_set) rules_to_remove.append(rule_a) changed = True @@ -821,7 +822,7 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe def find_inverted_counter(door, parent_counter, key_layout, world, player): # open all doors in counter - counter = open_all_counter(parent_counter, key_layout, door=door) + counter = open_all_counter(parent_counter, key_layout, world, player, door=door) max_counter = find_max_counter(key_layout) # find the difference inverted_counter = KeyCounter(key_layout.max_chests) @@ -837,7 +838,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player): return inverted_counter -def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): +def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False): changed = True counter = parent_counter proposed_doors = dict.fromkeys(parent_counter.open_doors.keys()) @@ -849,13 +850,11 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False): if skipBk: if not child.bigKey: doors_to_open[child] = None - elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened: + elif can_open_door_by_counter(child, counter, key_layout, world, player): doors_to_open[child] = None if len(doors_to_open.keys()) > 0: proposed_doors = {**proposed_doors, **doors_to_open} - bk_hint = counter.big_key_opened - for d in doors_to_open.keys(): - bk_hint = bk_hint or d.bigKey + bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys()) counter = find_counter(proposed_doors, bk_hint, key_layout, True) changed = True return counter @@ -1485,12 +1484,28 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player): return state.key_locations - state.used_smalls +def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): + if not world.keyshuffle[player] and not world.retro[player]: + bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 + avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) + return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) + return layout.max_chests + len(counter.key_only_locations) - counter.used_keys + + def cnt_avail_big_locations(ttl_locations, state, world, player): if not world.bigkeyshuffle[player]: return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0 return 1 if not state.big_key_special else 0 +def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player): + if not world.bigkeyshuffle[player]: + bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 + used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj + return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0 + return 1 if not layout.big_key_special else 0 + + def create_key_counters(key_layout, world, player): key_counters = {} key_layout.found_doors.clear() @@ -1500,12 +1515,11 @@ def create_key_counters(key_layout, world, player): state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) else: state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num - state.big_key_special, special_region = False, None + state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: if location.forced_big_key(): state.big_key_special = True - special_region = region for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and @@ -1531,7 +1545,7 @@ def create_key_counters(key_layout, world, player): if door.bigKey or door.name in special_big_key_doors: key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region): + if can_open_door(door, child_state, world, player): open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) @@ -1552,6 +1566,39 @@ def find_outside_connection(region): return None, None +def can_open_door(door, state, world, player): + if state.big_key_opened: + ttl_locations = count_free_locations(state, world, player) + else: + ttl_locations = count_locations_exclude_big_chest(state, world, player) + if door.smallKey: + ttl_small_key_only = count_small_key_only_locations(state) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + return available_small_locations > 0 + elif door.bigKey: + available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) + found_forced_bk = state.found_forced_bk() + return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk) + else: + return True + + +def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player): + if counter.big_key_opened: + ttl_locations = len(counter.free_locations) + else: + ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name]) + + if door.smallKey: + # ttl_small_key_only = len(counter.key_only_locations) + return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0 + elif door.bigKey: + available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) + return not counter.big_key_opened and available_big_locations > 0 and not layout.big_key_special + else: + return True + + def create_key_counter(state, key_layout, world, player): key_counter = KeyCounter(key_layout.max_chests) key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors))) From 4b29408911b61ec6ee4e04aaff7dcc13763868ca Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Aug 2021 13:49:46 -0600 Subject: [PATCH 023/104] Perf enhancement fixes --- Fill.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Fill.py b/Fill.py index d1905daf..9adb0916 100644 --- a/Fill.py +++ b/Fill.py @@ -564,10 +564,11 @@ def balance_multiworld_progression(world): replaced_items = False # sort then shuffle to maintain deterministic behaviour, # while allowing use of set for better algorithm growth behaviour elsewhere - replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked) - world.random.shuffle(replacement_locations) - items_to_replace.sort() - world.random.shuffle(items_to_replace) + replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked), + key=lambda loc: (loc.name, loc.player)) + random.shuffle(replacement_locations) + items_to_replace.sort(key=lambda item: (item.name, item.player)) + random.shuffle(items_to_replace) while replacement_locations and items_to_replace: old_location = items_to_replace.pop() for new_location in replacement_locations: From 6d98636626e07170e02768f880541ba98b7765c9 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 3 Aug 2021 17:28:20 -0500 Subject: [PATCH 024/104] Made flute pathing visible in spoiler log --- Regions.py | 2 +- Rules.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Regions.py b/Regions.py index c916716e..5147f158 100644 --- a/Regions.py +++ b/Regions.py @@ -6,7 +6,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType def create_regions(world, player): world.regions += [ create_menu_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q', 'Other World S&Q']), - create_menu_region(player, 'Flute Sky', None, ['Flute Spot 1', 'Flute Spot 2', 'Flute Spot 3', 'Flute Spot 4', 'Flute Spot 5', 'Flute Spot 6', 'Flute Spot 7', 'Flute Spot 8']), + create_cave_region(player, 'Flute Sky', None, None, ['Flute Spot 1', 'Flute Spot 2', 'Flute Spot 3', 'Flute Spot 4', 'Flute Spot 5', 'Flute Spot 6', 'Flute Spot 7', 'Flute Spot 8']), create_lw_region(player, 'Master Sword Meadow', ['Master Sword Pedestal'], ['Master Sword Meadow SC']), create_lw_region(player, 'Lost Woods West Area', None, ['Lost Woods Bush (West)', 'Skull Woods Back Mirror Spot', 'Skull Woods Forgotten (West) Mirror Spot', 'Skull Woods Forgotten (East) Mirror Spot', 'Skull Woods Portal Entry Mirror Spot', 'Lost Woods NW', 'Lost Woods SW', 'Lost Woods SC']), diff --git a/Rules.py b/Rules.py index ba46fe55..dc884638 100644 --- a/Rules.py +++ b/Rules.py @@ -174,8 +174,8 @@ def global_rules(world, player): exit.hide_path = True world.get_region('Flute Sky', player).can_reach_private = lambda state: True - for exit in world.get_region('Flute Sky', player).exits: - exit.hide_path = True + #for exit in world.get_region('Flute Sky', player).exits: + # exit.hide_path = True set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) From 917e8a920c16aa09c0486bc4c0c46a935f4dde0d Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 3 Aug 2021 17:33:45 -0500 Subject: [PATCH 025/104] Made flute pathing visible in spoiler log --- Regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Regions.py b/Regions.py index 5147f158..c916716e 100644 --- a/Regions.py +++ b/Regions.py @@ -6,7 +6,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType def create_regions(world, player): world.regions += [ create_menu_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q', 'Other World S&Q']), - create_cave_region(player, 'Flute Sky', None, None, ['Flute Spot 1', 'Flute Spot 2', 'Flute Spot 3', 'Flute Spot 4', 'Flute Spot 5', 'Flute Spot 6', 'Flute Spot 7', 'Flute Spot 8']), + create_menu_region(player, 'Flute Sky', None, ['Flute Spot 1', 'Flute Spot 2', 'Flute Spot 3', 'Flute Spot 4', 'Flute Spot 5', 'Flute Spot 6', 'Flute Spot 7', 'Flute Spot 8']), create_lw_region(player, 'Master Sword Meadow', ['Master Sword Pedestal'], ['Master Sword Meadow SC']), create_lw_region(player, 'Lost Woods West Area', None, ['Lost Woods Bush (West)', 'Skull Woods Back Mirror Spot', 'Skull Woods Forgotten (West) Mirror Spot', 'Skull Woods Forgotten (East) Mirror Spot', 'Skull Woods Portal Entry Mirror Spot', 'Lost Woods NW', 'Lost Woods SW', 'Lost Woods SC']), From d71a246dc1ec4869e43552c2017507c07fa4f226 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 3 Aug 2021 20:30:25 -0500 Subject: [PATCH 026/104] Excluding water regions from flute access --- BaseClasses.py | 1 + OverworldShuffle.py | 8 +++--- Regions.py | 60 ++++++++++++++++++++++++--------------------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 09ff5b3b..fda63d58 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1159,6 +1159,7 @@ class Region(object): self.is_light_world = False # will be set aftermaking connections. self.is_dark_world = False self.spot_type = 'Region' + self.terrain = None self.hint_text = hint self.recursion_count = 0 self.player = player diff --git a/OverworldShuffle.py b/OverworldShuffle.py index f2a0bb4a..dc51abab 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -501,10 +501,10 @@ def reorganize_groups(world, groups, player): raise NotImplementedError('Shuffling not supported yet') def create_flute_exits(world, player): - for region in world.regions: - if ((region.type == RegionType.LightWorld and region.name not in world.owswaps[player][1]) - or (region.type == RegionType.DarkWorld and region.name in world.owswaps[player][2])) \ - and region.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']: + for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): + if (world.owSwap[player] != 'mixed' and region.type == RegionType.LightWorld) \ + or (world.owSwap[player] == 'mixed' and region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ + and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) exit.access_rule = lambda state: state.can_flute(player) diff --git a/Regions.py b/Regions.py index c916716e..9ce0abbc 100644 --- a/Regions.py +++ b/Regions.py @@ -1,6 +1,6 @@ import collections from Items import ItemFactory -from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType +from BaseClasses import Region, Location, Entrance, RegionType, Terrain, Shop, ShopType def create_regions(world, player): @@ -30,7 +30,7 @@ def create_regions(world, player): create_lw_region(player, 'Mountain Entry Entrance', None, ['Mountain Entry Entrance Rock (East)', 'Mountain Entry Entrance Ledge Drop', 'Old Man Cave (West)', 'Bumper Cave Entry Mirror Spot']), create_lw_region(player, 'Mountain Entry Ledge', None, ['Mountain Entry Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot']), create_lw_region(player, 'Zora Waterfall Area', None, ['Zora Waterfall Water Entry', 'Catfish Mirror Spot', 'Zora Waterfall SE', 'Zora Waterfall NE']), - create_lw_region(player, 'Zora Waterfall Water', None, ['Waterfall of Wishing Cave Entry', 'Zora Waterfall Landing', 'Zora Whirlpool']), + create_lw_region(player, 'Zora Waterfall Water', None, ['Waterfall of Wishing Cave Entry', 'Zora Waterfall Landing', 'Zora Whirlpool'], Terrain.Water), create_lw_region(player, 'Waterfall of Wishing Cave', None, ['Zora Waterfall Water Drop', 'Waterfall of Wishing']), create_lw_region(player, 'Zoras Domain', ['King Zora', 'Zora\'s Ledge'], ['Zoras Domain SW']), create_lw_region(player, 'Lost Woods Pass West Area', None, ['Skull Woods Pass West Mirror Spot', 'Lost Woods Pass NW', 'Lost Woods Pass SW']), @@ -45,13 +45,13 @@ def create_regions(world, player): create_lw_region(player, 'Kings Grave Area', None, ['Kings Grave Inner Rocks', 'Kings Grave', 'Dark Graveyard Grave Mirror Spot']), create_lw_region(player, 'River Bend Area', None, ['North Fairy Cave Drop', 'River Bend Water Drop', 'North Fairy Cave', 'Qirn Jump Mirror Spot', 'River Bend WC', 'River Bend SW']), create_lw_region(player, 'River Bend East Bank', None, ['River Bend East Water Drop', 'Qirn Jump East Mirror Spot', 'River Bend SE', 'River Bend EC', 'River Bend ES']), - create_lw_region(player, 'River Bend Water', None, ['River Bend West Pier', 'River Bend East Pier', 'River Bend EN', 'River Bend SC', 'River Bend Whirlpool']), + create_lw_region(player, 'River Bend Water', None, ['River Bend West Pier', 'River Bend East Pier', 'River Bend EN', 'River Bend SC', 'River Bend Whirlpool'], Terrain.Water), create_lw_region(player, 'Potion Shop Area', None, ['Potion Shop Water Drop', 'Potion Shop Rock (South)', 'Potion Shop', 'Dark Witch Mirror Spot', 'Potion Shop WC', 'Potion Shop WS']), create_lw_region(player, 'Potion Shop Northeast', None, ['Potion Shop Northeast Water Drop', 'Potion Shop Rock (North)', 'Dark Witch Northeast Mirror Spot', 'Potion Shop EC']), - create_lw_region(player, 'Potion Shop Water', None, ['Potion Shop WN', 'Potion Shop EN']), + create_lw_region(player, 'Potion Shop Water', None, ['Potion Shop WN', 'Potion Shop EN'], Terrain.Water), create_lw_region(player, 'Zora Approach Area', None, ['Zora Approach Rocks (West)', 'Zora Approach Bottom Ledge Drop', 'Zora Approach Water Drop', 'Catfish Approach Mirror Spot', 'Zora Approach WC']), create_lw_region(player, 'Zora Approach Ledge', None, ['Zora Approach Rocks (East)', 'Zora Approach Ledge Drop', 'Catfish Approach Ledge Mirror Spot', 'Zora Approach NE']), - create_lw_region(player, 'Zora Approach Water', None, ['Zora Approach WN']), + create_lw_region(player, 'Zora Approach Water', None, ['Zora Approach WN'], Terrain.Water), create_lw_region(player, 'Kakariko Area', ['Bottle Merchant'], ['Kakariko Southwest Bush (North)', 'Kakariko Yard Bush (South)', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Blinds Hideout', 'Elder House (West)', 'Elder House (East)', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Chicken House', 'Sick Kids House', 'Kakariko Shop', 'Tavern (Front)', 'Tavern North', 'Village of Outcasts Mirror Spot', 'Kakariko NW', 'Kakariko NC', 'Kakariko NE', 'Kakariko ES', 'Kakariko SE']), @@ -66,7 +66,7 @@ def create_regions(world, player): create_lw_region(player, 'Hyrule Castle East Entry', None, ['Hyrule Castle Outer East Rock', 'Pyramid Entry Mirror Spot', 'Hyrule Castle ES']), create_lw_region(player, 'Wooden Bridge Area', None, ['Wooden Bridge Bush (South)', 'Wooden Bridge Water Drop', 'Broken Bridge West Mirror Spot', 'Broken Bridge East Mirror Spot', 'Wooden Bridge NW', 'Wooden Bridge SW']), create_lw_region(player, 'Wooden Bridge Northeast', None, ['Wooden Bridge Bush (North)', 'Wooden Bridge Northeast Water Drop', 'Broken Bridge Northeast Mirror Spot', 'Wooden Bridge NE']), - create_lw_region(player, 'Wooden Bridge Water', None, ['Wooden Bridge NC']), + create_lw_region(player, 'Wooden Bridge Water', None, ['Wooden Bridge NC'], Terrain.Water), create_lw_region(player, 'Eastern Palace Area', None, ['Sahasrahlas Hut', 'Eastern Palace', 'Palace of Darkness Mirror Spot', 'Eastern Palace SW', 'Eastern Palace SE']), create_lw_region(player, 'Eastern Cliff', None, ['Sand Dunes Ledge Drop', 'Stone Bridge East Ledge Drop', 'Tree Line Ledge Drop', 'Eastern Palace Ledge Drop']), create_lw_region(player, 'Blacksmith Area', None, ['Blacksmiths Hut', 'Bat Cave Cave', 'Bat Cave Ledge Peg', 'Hammer Pegs Mirror Spot', 'Hammer Pegs Entry Mirror Spot', 'Blacksmith WS']), @@ -81,11 +81,11 @@ def create_regions(world, player): create_lw_region(player, 'Central Bonk Rocks Area', None, ['Bonk Fairy (Light)', 'Dark Bonk Rocks Mirror Spot', 'Central Bonk Rocks NW', 'Central Bonk Rocks SW', 'Central Bonk Rocks EN', 'Central Bonk Rocks EC', 'Central Bonk Rocks ES']), create_lw_region(player, 'Links House Area', None, ['Links House', 'Big Bomb Shop Mirror Spot', 'Links House NE', 'Links House WN', 'Links House WC', 'Links House WS', 'Links House SC', 'Links House ES']), create_lw_region(player, 'Stone Bridge Area', None, ['Hammer Bridge North Mirror Spot', 'Hammer Bridge South Mirror Spot', 'Stone Bridge NC', 'Stone Bridge EN', 'Stone Bridge WS', 'Stone Bridge SC']), - create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC']), + create_lw_region(player, 'Stone Bridge Water', None, ['Dark Hobo Mirror Spot', 'Stone Bridge WC', 'Stone Bridge EC'], Terrain.Water), create_lw_region(player, 'Hobo Bridge', ['Hobo'], ['Hobo EC']), create_lw_region(player, 'Central Cliffs', None, ['Central Bonk Rocks Cliff Ledge Drop', 'Links House Cliff Ledge Drop', 'Stone Bridge Cliff Ledge Drop', 'Lake Hylia Area Cliff Ledge Drop', 'Lake Hylia Island FAWT Ledge Drop', 'Stone Bridge EC Cliff Water Drop', 'Tree Line WC Cliff Water Drop', 'C Whirlpool Outer Cliff Ledge Drop', 'C Whirlpool Cliff Ledge Drop', 'South Teleporter Cliff Ledge Drop', 'Statues Cliff Ledge Drop']), create_lw_region(player, 'Tree Line Area', None, ['Lake Hylia Fairy', 'Dark Tree Line Mirror Spot', 'Tree Line WN', 'Tree Line NW', 'Tree Line SE']), - create_lw_region(player, 'Tree Line Water', None, ['Tree Line WC', 'Tree Line SC']), + create_lw_region(player, 'Tree Line Water', None, ['Tree Line WC', 'Tree Line SC'], Terrain.Water), create_lw_region(player, 'Eastern Nook Area', None, ['Long Fairy Cave', 'Darkness Nook Mirror Spot', 'East Hyrule Teleporter', 'Eastern Nook NE']), create_lw_region(player, 'Desert Area', None, ['Desert Palace Statue Move', 'Checkerboard Ledge Approach', 'Aginahs Cave', 'Misery Mire Mirror Spot', 'Desert ES']), create_lw_region(player, 'Desert Ledge', ['Desert Ledge'], ['Desert Ledge Outer Rocks', 'Desert Ledge Drop', 'Desert Palace Entrance (West)', 'Misery Mire Ledge Mirror Spot']), @@ -100,16 +100,16 @@ def create_regions(world, player): create_lw_region(player, 'Flute Boy Bush Entry', None, ['Flute Boy Bush (North)', 'Stumpy Bush Entry Mirror Spot', 'Flute Boy Approach NC']), create_lw_region(player, 'Cave 45 Ledge', None, ['Cave 45 Inverted Leave', 'Cave 45 Ledge Drop', 'Cave 45']), create_lw_region(player, 'C Whirlpool Area', None, ['C Whirlpool Rock (Bottom)', 'C Whirlpool Water Entry', 'Dark C Whirlpool Mirror Spot', 'South Hyrule Teleporter', 'C Whirlpool EN', 'C Whirlpool ES', 'C Whirlpool SC']), - create_lw_region(player, 'C Whirlpool Water', None, ['C Whirlpool Landing', 'C Whirlpool', 'C Whirlpool EC']), + create_lw_region(player, 'C Whirlpool Water', None, ['C Whirlpool Landing', 'C Whirlpool', 'C Whirlpool EC'], Terrain.Water), create_lw_region(player, 'C Whirlpool Outer Area', None, ['C Whirlpool Rock (Top)', 'Dark C Whirlpool Outer Mirror Spot', 'C Whirlpool WC', 'C Whirlpool NW']), create_lw_region(player, 'Statues Area', None, ['Statues Water Entry', 'Light Hype Fairy', 'Hype Cave Mirror Spot', 'Statues NC', 'Statues WN', 'Statues WS', 'Statues SC']), - create_lw_region(player, 'Statues Water', None, ['Statues Landing', 'Statues WC']), + create_lw_region(player, 'Statues Water', None, ['Statues Landing', 'Statues WC'], Terrain.Water), create_lw_region(player, 'Lake Hylia Area', None, ['Lake Hylia Water Drop', 'Lake Hylia Fortune Teller', 'Cave Shop (Lake Hylia)', 'Ice Lake Mirror Spot', 'Lake Hylia NW']), create_lw_region(player, 'Lake Hylia South Shore', None, ['Lake Hylia South Water Drop', 'Mini Moldorm Cave', 'Ice Lake Southwest Mirror Spot', 'Ice Lake Southeast Mirror Spot', 'Lake Hylia WS', 'Lake Hylia ES']), create_lw_region(player, 'Lake Hylia Northeast Bank', None, ['Lake Hylia Northeast Water Drop', 'Ice Lake Northeast Mirror Spot', 'Lake Hylia NE']), create_lw_region(player, 'Lake Hylia Central Island', None, ['Lake Hylia Central Water Drop', 'Capacity Upgrade', 'Ice Palace Mirror Spot', 'Lake Hylia Teleporter']), create_lw_region(player, 'Lake Hylia Island', ['Lake Hylia Island'], ['Lake Hylia Island Water Drop']), - create_lw_region(player, 'Lake Hylia Water', None, ['Lake Hylia Central Island Pier', 'Lake Hylia Island Pier', 'Lake Hylia West Pier', 'Lake Hylia East Pier', 'Lake Hylia NC', 'Lake Hylia EC', 'Lake Hylia Whirlpool']), + create_lw_region(player, 'Lake Hylia Water', None, ['Lake Hylia Central Island Pier', 'Lake Hylia Island Pier', 'Lake Hylia West Pier', 'Lake Hylia East Pier', 'Lake Hylia NC', 'Lake Hylia EC', 'Lake Hylia Whirlpool'], Terrain.Water), create_lw_region(player, 'Ice Cave Area', None, ['Ice Rod Cave', 'Good Bee Cave', '20 Rupee Cave', 'Shopping Mall Mirror Spot', 'Ice Cave SE', 'Ice Cave SW']), create_lw_region(player, 'Desert Pass Area', ['Purple Chest'], ['Desert Pass Ladder (South)', 'Desert Fairy', '50 Rupee Cave', 'Swamp Nook Mirror Spot', 'Desert Pass WS', 'Desert Pass EC', 'Desert Pass Rocks (North)']), create_lw_region(player, 'Desert Pass Southeast', None, ['Desert Pass Rocks (South)', 'Swamp Nook Southeast Mirror Spot', 'Desert Pass ES']), @@ -117,8 +117,8 @@ def create_regions(world, player): create_lw_region(player, 'Dam Area', ['Sunken Treasure'], ['Dam', 'Swamp Mirror Spot', 'Dam WC', 'Dam WS', 'Dam NC', 'Dam EC']), create_lw_region(player, 'South Pass Area', None, ['Dark South Pass Mirror Spot', 'South Pass WC', 'South Pass NC', 'South Pass ES']), create_lw_region(player, 'Octoballoon Area', None, ['Octoballoon Water Drop', 'Bomber Corner Mirror Spot', 'Octoballoon WS', 'Octoballoon NE']), - create_lw_region(player, 'Octoballoon Water', None, ['Octoballoon Pier', 'Octoballoon WC', 'Octoballoon Whirlpool']), - create_lw_region(player, 'Octoballoon Water Ledge', None, ['Octoballoon Waterfall Water Drop', 'Octoballoon NW']), + create_lw_region(player, 'Octoballoon Water', None, ['Octoballoon Pier', 'Octoballoon WC', 'Octoballoon Whirlpool'], Terrain.Water), + create_lw_region(player, 'Octoballoon Water Ledge', None, ['Octoballoon Waterfall Water Drop', 'Octoballoon NW'], Terrain.Water), create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods Bush Rock (East)', 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Lost Woods East Mirror Spot', 'Skull Woods SE']), @@ -151,13 +151,13 @@ def create_regions(world, player): create_dw_region(player, 'Dark Graveyard Area', None, ['Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Dark Graveyard WC', 'Dark Graveyard EC']), create_dw_region(player, 'Qirn Jump Area', None, ['Qirn Jump Water Drop', 'River Bend Mirror Spot', 'Qirn Jump WC', 'Qirn Jump SW']), create_dw_region(player, 'Qirn Jump East Bank', None, ['Qirn Jump East Water Drop', 'River Bend East Mirror Spot', 'Qirn Jump SE', 'Qirn Jump EC', 'Qirn Jump ES']), - create_dw_region(player, 'Qirn Jump Water', None, ['Qirn Jump Pier', 'Qirn Jump Whirlpool', 'Qirn Jump EN', 'Qirn Jump SC']), + create_dw_region(player, 'Qirn Jump Water', None, ['Qirn Jump Pier', 'Qirn Jump Whirlpool', 'Qirn Jump EN', 'Qirn Jump SC'], Terrain.Water), create_dw_region(player, 'Dark Witch Area', None, ['Dark Witch Water Drop', 'Dark Witch Rock (South)', 'Dark World Potion Shop', 'Potion Shop Mirror Spot', 'Dark Witch WC', 'Dark Witch WS']), create_dw_region(player, 'Dark Witch Northeast', None, ['Dark Witch Northeast Water Drop', 'Dark Witch Rock (North)', 'Potion Shop Northeast Mirror Spot', 'Dark Witch EC']), - create_dw_region(player, 'Dark Witch Water', None, ['Dark Witch WN', 'Dark Witch EN']), + create_dw_region(player, 'Dark Witch Water', None, ['Dark Witch WN', 'Dark Witch EN'], Terrain.Water), create_dw_region(player, 'Catfish Approach Area', None, ['Catfish Approach Rocks (West)', 'Catfish Approach Bottom Ledge Drop', 'Catfish Approach Water Drop', 'Zora Approach Mirror Spot', 'Catfish Approach WC']), create_dw_region(player, 'Catfish Approach Ledge', None, ['Catfish Approach Rocks (East)', 'Catfish Approach Ledge Drop', 'Zora Approach Ledge Mirror Spot', 'Catfish Approach NE']), - create_dw_region(player, 'Catfish Approach Water', None, ['Catfish Approach WN']), + create_dw_region(player, 'Catfish Approach Water', None, ['Catfish Approach WN'], Terrain.Water), create_dw_region(player, 'Village of Outcasts Area', None, ['Village of Outcasts Pegs', 'Chest Game', 'Thieves Town', 'C-Shaped House', 'Brewery', 'Kakariko Mirror Spot', 'Village of Outcasts NW', 'Village of Outcasts NC', 'Village of Outcasts NE', 'Village of Outcasts ES', 'Village of Outcasts SE']), create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop', 'Kakariko Grass Mirror Spot']), create_dw_region(player, 'Shield Shop Area', None, ['Shield Shop Fence (Outer) Ledge Drop', 'Forgotton Forest Mirror Spot', 'Shield Shop NW', 'Shield Shop NE']), @@ -168,7 +168,7 @@ def create_regions(world, player): create_dw_region(player, 'Broken Bridge Area', None, ['Broken Bridge Hammer Rock (South)', 'Broken Bridge Water Drop', 'Wooden Bridge Mirror Spot', 'Broken Bridge SW']), create_dw_region(player, 'Broken Bridge Northeast', None, ['Broken Bridge Hammer Rock (North)', 'Broken Bridge Hookshot Gap', 'Broken Bridge Northeast Water Drop', 'Wooden Bridge Northeast Mirror Spot', 'Broken Bridge NE']), create_dw_region(player, 'Broken Bridge West', None, ['Broken Bridge West Water Drop', 'Wooden Bridge West Mirror Spot', 'Broken Bridge NW']), - create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC']), + create_dw_region(player, 'Broken Bridge Water', None, ['Broken Bridge NC'], Terrain.Water), create_dw_region(player, 'Palace of Darkness Area', None, ['Palace of Darkness Hint', 'Palace of Darkness', 'Eastern Palace Mirror Spot', 'Palace of Darkness SW', 'Palace of Darkness SE']), create_dw_region(player, 'Darkness Cliff', None, ['Dark Dunes Ledge Drop', 'Hammer Bridge North Ledge Drop', 'Dark Tree Line Ledge Drop', 'Palace of Darkness Ledge Drop']), create_dw_region(player, 'Hammer Pegs Entry', None, ['Peg Area Rocks (West)', 'Blacksmith Entry Mirror Spot', 'Hammer Pegs WS']), @@ -185,11 +185,11 @@ def create_regions(world, player): create_dw_region(player, 'Big Bomb Shop Area', None, ['Big Bomb Shop', 'Links House Mirror Spot', 'Big Bomb Shop NE', 'Big Bomb Shop WN', 'Big Bomb Shop WC', 'Big Bomb Shop WS', 'Big Bomb Shop SC', 'Big Bomb Shop ES']), create_dw_region(player, 'Hammer Bridge North Area', None, ['Hammer Bridge Pegs (North)', 'Hammer Bridge Water Drop', 'Stone Bridge Mirror Spot', 'Hammer Bridge NC', 'Hammer Bridge EN']), create_dw_region(player, 'Hammer Bridge South Area', None, ['Hammer Bridge Pegs (South)', 'Stone Bridge South Mirror Spot', 'Hammer Bridge WS', 'Hammer Bridge SC']), - create_dw_region(player, 'Hammer Bridge Water', None, ['Hammer Bridge Pier', 'Hobo Mirror Spot', 'Hammer Bridge EC']), + create_dw_region(player, 'Hammer Bridge Water', None, ['Hammer Bridge Pier', 'Hobo Mirror Spot', 'Hammer Bridge EC'], Terrain.Water), create_dw_region(player, 'Dark Central Cliffs', None, ['Dark Bonk Rocks Cliff Ledge Drop', 'Bomb Shop Cliff Ledge Drop', 'Hammer Bridge South Cliff Ledge Drop', 'Ice Lake Area Cliff Ledge Drop', 'Ice Palace Island FAWT Ledge Drop', 'Hammer Bridge EC Cliff Water Drop', 'Dark Tree Line WC Cliff Water Drop', 'Dark C Whirlpool Outer Cliff Ledge Drop', 'Dark C Whirlpool Cliff Ledge Drop', 'Hype Cliff Ledge Drop', 'Dark South Teleporter Cliff Ledge Drop']), create_dw_region(player, 'Dark Tree Line Area', None, ['Dark Lake Hylia Fairy', 'Tree Line Mirror Spot', 'Dark Tree Line WN', 'Dark Tree Line NW', 'Dark Tree Line SE']), - create_dw_region(player, 'Dark Tree Line Water', None, ['Dark Tree Line WC', 'Dark Tree Line SC']), + create_dw_region(player, 'Dark Tree Line Water', None, ['Dark Tree Line WC', 'Dark Tree Line SC'], Terrain.Water), create_dw_region(player, 'Palace of Darkness Nook Area', None, ['East Dark World Hint', 'East Dark World Teleporter', 'Eastern Nook Mirror Spot', 'Palace of Darkness Nook NE']), create_dw_region(player, 'Misery Mire Area', None, ['Mire Shed', 'Misery Mire', 'Dark Desert Fairy', 'Dark Desert Hint', 'Desert Mirror Spot', 'Desert Ledge Mirror Spot', 'Checkerboard Mirror Spot', 'DP Stairs Mirror Spot', 'DP Entrance (North) Mirror Spot']), create_dw_region(player, 'Misery Mire Teleporter Ledge', None, ['Misery Mire Teleporter Ledge Drop', 'Misery Mire Teleporter']), @@ -197,15 +197,15 @@ def create_regions(world, player): create_dw_region(player, 'Stumpy Approach Area', None, ['Stumpy Approach Bush (South)', 'Cave 45 Mirror Spot', 'Stumpy Approach NW', 'Stumpy Approach EC']), create_dw_region(player, 'Stumpy Approach Bush Entry', None, ['Stumpy Approach Bush (North)', 'Flute Boy Entry Mirror Spot', 'Stumpy Approach NC']), create_dw_region(player, 'Dark C Whirlpool Area', None, ['Dark C Whirlpool Rock (Bottom)', 'South Dark World Teleporter', 'C Whirlpool Mirror Spot', 'Dark C Whirlpool Water Entry', 'Dark C Whirlpool EN', 'Dark C Whirlpool ES', 'Dark C Whirlpool SC']), - create_dw_region(player, 'Dark C Whirlpool Water', None, ['Dark C Whirlpool Landing', 'Dark C Whirlpool EC']), + create_dw_region(player, 'Dark C Whirlpool Water', None, ['Dark C Whirlpool Landing', 'Dark C Whirlpool EC'], Terrain.Water), create_dw_region(player, 'Dark C Whirlpool Outer Area', None, ['Dark C Whirlpool Rock (Top)', 'C Whirlpool Outer Mirror Spot', 'Dark C Whirlpool WC', 'Dark C Whirlpool NW']), create_dw_region(player, 'Hype Cave Area', None, ['Hype Cave Water Entry', 'Hype Cave', 'Statues Mirror Spot', 'Hype Cave NC', 'Hype Cave WN', 'Hype Cave WS', 'Hype Cave SC']), - create_dw_region(player, 'Hype Cave Water', None, ['Hype Cave Landing', 'Hype Cave WC']), + create_dw_region(player, 'Hype Cave Water', None, ['Hype Cave Landing', 'Hype Cave WC'], Terrain.Water), create_dw_region(player, 'Ice Lake Area', None, ['Ice Lake Water Drop', 'Dark Lake Hylia Shop', 'Lake Hylia Mirror Spot', 'Ice Lake NW']), create_dw_region(player, 'Ice Lake Northeast Bank', None, ['Ice Lake Northeast Water Drop', 'Lake Hylia Northeast Mirror Spot', 'Ice Lake NE']), create_dw_region(player, 'Ice Lake Ledge (West)', None, ['Ice Lake Southwest Water Drop', 'South Shore Mirror Spot', 'Ice Lake WS']), create_dw_region(player, 'Ice Lake Ledge (East)', None, ['Ice Lake Southeast Water Drop', 'South Shore East Mirror Spot', 'Ice Lake ES']), - create_dw_region(player, 'Ice Lake Water', None, ['Ice Lake Northeast Pier', 'Lake Hylia Island Mirror Spot', 'Ice Lake NC', 'Ice Lake EC']), + create_dw_region(player, 'Ice Lake Water', None, ['Ice Lake Northeast Pier', 'Lake Hylia Island Mirror Spot', 'Ice Lake NC', 'Ice Lake EC'], Terrain.Water), create_dw_region(player, 'Ice Lake Moat', None, ['Ice Lake Moat Water Entry', 'Ice Lake Northeast Pier Bomb Jump', 'Ice Palace Approach', 'Lake Hylia Water Mirror Spot']), create_dw_region(player, 'Ice Palace Area', None, ['Ice Palace Leave', 'Ice Palace', 'Ice Palace Teleporter', 'Lake Hylia Central Island Mirror Spot']), create_dw_region(player, 'Shopping Mall Area', None, ['Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Ice Cave Mirror Spot', 'Shopping Mall SW', 'Shopping Mall SE']), @@ -213,8 +213,8 @@ def create_regions(world, player): create_dw_region(player, 'Swamp Area', None, ['Swamp Palace', 'Dam Mirror Spot', 'Swamp WC', 'Swamp WS', 'Swamp NC', 'Swamp EC']), create_dw_region(player, 'Dark South Pass Area', None, ['South Pass Mirror Spot', 'Dark South Pass WC', 'Dark South Pass NC', 'Dark South Pass ES']), create_dw_region(player, 'Bomber Corner Area', None, ['Bomber Corner Water Drop', 'Octoballoon Mirror Spot', 'Bomber Corner WS', 'Bomber Corner NE']), - create_dw_region(player, 'Bomber Corner Water', None, ['Bomber Corner Pier', 'Bomber Corner Whirlpool', 'Bomber Corner WC']), - create_dw_region(player, 'Bomber Corner Water Ledge', None, ['Bomber Corner Waterfall Water Drop', 'Bomber Corner NW']), + create_dw_region(player, 'Bomber Corner Water', None, ['Bomber Corner Pier', 'Bomber Corner Whirlpool', 'Bomber Corner WC'], Terrain.Water), + create_dw_region(player, 'Bomber Corner Water Ledge', None, ['Bomber Corner Waterfall Water Drop', 'Bomber Corner NW'], Terrain.Water), create_cave_region(player, 'Lost Woods Gamble', 'a game of chance'), create_cave_region(player, 'Lost Woods Hideout (top)', 'a drop\'s exit', ['Lost Woods Hideout'], ['Lost Woods Hideout (top to bottom)']), @@ -1013,12 +1013,16 @@ def create_menu_region(player, name, locations=None, exits=None): return _create_region(player, name, RegionType.Menu, 'Menu', locations, exits) -def create_lw_region(player, name, locations=None, exits=None): - return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) +def create_lw_region(player, name, locations=None, exits=None, terrain=Terrain.Land): + region = _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits) + region.terrain = terrain + return region -def create_dw_region(player, name, locations=None, exits=None): - return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits) +def create_dw_region(player, name, locations=None, exits=None, terrain=Terrain.Land): + region = _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits) + region.terrain = terrain + return region def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None): From 8677a0313c3861b7f74d67d67805fed815c8ba73 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 3 Aug 2021 20:36:10 -0500 Subject: [PATCH 027/104] Fix LW/DW marking in copy_world --- Main.py | 4 +++- OverworldShuffle.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Main.py b/Main.py index 6bf42916..a9ebf3cf 100644 --- a/Main.py +++ b/Main.py @@ -16,7 +16,7 @@ from OverworldGlitchRules import create_owg_connections from PotShuffle import shuffle_pots from Regions import create_regions, create_shops, mark_light_world_regions, mark_dark_world_regions, create_dungeon_regions, adjust_locations from OWEdges import create_owedges -from OverworldShuffle import link_overworld, create_flute_exits +from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits from EntranceShuffle import link_entrances from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Doors import create_doors @@ -162,6 +162,7 @@ def main(args, seed=None, fish=None): for player in range(1, world.players + 1): link_overworld(world, player) + update_world_regions(world, player) create_flute_exits(world, player) logger.info(world.fish.translate("cli","cli","shuffling.world")) @@ -421,6 +422,7 @@ def copy_world(world): for player in range(1, world.players + 1): create_regions(ret, player) + update_world_regions(ret, player) create_flute_exits(ret, player) create_dungeon_regions(ret, player) create_shops(ret, player) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index dc51abab..e80713ae 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -135,13 +135,7 @@ def link_overworld(world, player): assert len(swapped_edges) == 0 #move swapped regions to other world - if world.owSwap[player] == 'mixed': - for name in world.owswaps[player][1]: - region = world.get_region(name, player) - region.type = RegionType.DarkWorld - for name in world.owswaps[player][2]: - region = world.get_region(name, player) - region.type = RegionType.LightWorld + update_world_regions(world, player) # make new connections for owid in ow_connections.keys(): @@ -512,6 +506,13 @@ def create_flute_exits(world, player): region.exits.append(exit) world.initialize_regions() +def update_world_regions(world, player): + if world.owSwap[player] == 'mixed': + for name in world.owswaps[player][1]: + world.get_region(name, player).type = RegionType.DarkWorld + for name in world.owswaps[player][2]: + world.get_region(name, player).type = RegionType.LightWorld + test_connections = [ #('Links House ES', 'Octoballoon WS'), #('Links House NE', 'Lost Woods Pass SW') From d7e326f71dba53d5d2acd0a5ad5b9128e2d18dea Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 4 Aug 2021 12:08:17 -0600 Subject: [PATCH 028/104] Key checking in dungeons limited to single player for speed Spoiler traversal fixes and speed Bug with boss shuffle spoiler in MW fixed Perf enhancement fix for pot shuffle --- BaseClasses.py | 16 ++++++++++------ Main.py | 7 +++++-- Rom.py | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 087bda1b..d6e0c5e2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -518,7 +518,8 @@ class CollectionState(object): connection, crystal_state = queue.popleft() new_region = connection.connected_region if not self.should_visit(new_region, rrp, crystal_state, player): - bc.pop(connection, None) + if not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): + bc.pop(connection, None) elif connection.can_reach(self): bc.pop(connection, None) if new_region.type == RegionType.Dungeon: @@ -643,9 +644,12 @@ class CollectionState(object): while not done: rrp_ = child_state.reachable_regions[player] bc_ = child_state.blocked_connections[player] - self.set_dungeon_limits(player, dungeon_name) + child_state.set_dungeon_limits(player, dungeon_name) + child_queue.extend([(x, y) for x, y in bc_.items() + if child_state.possibly_connected_to_dungeon(x.parent_region, + player)]) child_state.traverse_world(child_queue, rrp_, bc_, player) - new_events = child_state.sweep_for_events_once() + new_events = child_state.sweep_for_events_once(player) child_state.stale[player] = False if new_events: for conn in bc_: @@ -890,8 +894,8 @@ class CollectionState(object): return spot.can_reach(self) - def sweep_for_events_once(self): - locations = self.world.get_filled_locations() + def sweep_for_events_once(self, player): + locations = self.world.get_filled_locations(player) checked_locations = set([l for l in locations if l in self.locations_checked]) reachable_events = [location for location in locations if location.event and location.can_reach(self)] reachable_events = self._do_not_flood_the_keys(reachable_events) @@ -2520,7 +2524,7 @@ class Spoiler(object): for player in range(1, self.world.players + 1): if self.world.boss_shuffle[player] != 'none': - bossmap = self.bosses[player] if self.world.players > 1 else self.bosses + bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) diff --git a/Main.py b/Main.py index 719d0cc5..d8967f1f 100644 --- a/Main.py +++ b/Main.py @@ -573,7 +573,7 @@ def create_playthrough(world): old_item = location.item location.item = None # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic - world.clear_exp_cache() + # world.clear_exp_cache() if world.can_beat_game(state_cache[num]): # logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') to_delete.add(location) @@ -615,7 +615,10 @@ def create_playthrough(world): logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations)) if not sphere: - raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) + if world.has_beaten_game(state): + required_locations.clear() + else: + raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required")) # store the required locations for statistical analysis old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] diff --git a/Rom.py b/Rom.py index 564126fb..f74a385e 100644 --- a/Rom.py +++ b/Rom.py @@ -2795,7 +2795,7 @@ def write_pots_to_rom(rom, pot_contents): pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing] if len(pots) > 0: write_int16(rom, pot_item_room_table_lookup + 2*i, n) - rom.write_bytes(n, itertools.chain(*((pot.x,pot.y,pot.item) for pot in pots))) + rom.write_bytes(n, list(itertools.chain.from_iterable(((pot.x, pot.y, pot.item) for pot in pots)))) n += 3*len(pots) + 2 rom.write_bytes(n - 2, [0xFF,0xFF]) else: From feb9177871b6015a7b7ee7f32159902dcc934722 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 4 Aug 2021 13:46:30 -0600 Subject: [PATCH 029/104] Ignore certain rules when bk isn't available in contradiction checking --- KeyDoorShuffle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7d550d1b..5dff33ef 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -127,6 +127,10 @@ class PlacementRule(object): rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk if check_locations is None or rule_locations is None: return False + if not bk_blocked and big_key_loc not in check_locations: # bk is not available, so rule doesn't apply + return False + if not rule_blocked and big_key_loc not in rule_locations: # bk is not available, so rule doesn't apply + return False check_locations = check_locations - {big_key_loc} rule_locations = rule_locations - {big_key_loc} threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk From 9fa89d9d3d5a041ca577f6089997b542d9c5d330 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sun, 8 Aug 2021 18:38:51 -0500 Subject: [PATCH 030/104] Fixed issue with vanilla OW failing to pick a filename --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index a9ebf3cf..d59588f1 100644 --- a/Main.py +++ b/Main.py @@ -263,7 +263,7 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owSwap[1] != 'vanilla' or world.seed.startsWith('M'): + if world.owShuffle[1] != 'vanilla' or world.owSwap[1] != 'vanilla' or str(world.seed).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' From 44efe51e022a8fe509e95344085989791c038f9f Mon Sep 17 00:00:00 2001 From: StructuralMike <66819228+StructuralMike@users.noreply.github.com> Date: Wed, 11 Aug 2021 10:22:54 +0200 Subject: [PATCH 031/104] Changing the cli to be bombbags over bomblogic Bombbags seemed better over bomblogic which is more ambiguous when considering new stuff OW and the bomb-only modes. Also added bombbags to the example yaml --- BaseClasses.py | 8 ++--- CLI.py | 4 +-- ItemList.py | 36 +++++++++---------- Main.py | 4 +-- Mystery.py | 2 +- RELEASENOTES.md | 4 +-- Rom.py | 6 ++-- Rules.py | 2 +- mystery_example.yml | 3 ++ resources/app/cli/args.json | 2 +- resources/app/cli/lang/en.json | 2 +- resources/app/gui/lang/en.json | 2 +- resources/app/gui/randomize/item/widgets.json | 2 +- source/classes/constants.py | 2 +- 14 files changed, 41 insertions(+), 38 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b7a80af7..e8e471c9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -114,7 +114,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) - set_player_attr('bomblogic', False) + set_player_attr('bombbags', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') set_player_attr('enemy_shuffle', 'none') @@ -687,7 +687,7 @@ class CollectionState(object): # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - return (not self.world.bomblogic[player] or self.has('Bomb Upgrade (+10)', player)) + return (not self.world.bombbags[player] or self.has('Bomb Upgrade (+10)', player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -2014,7 +2014,7 @@ class Spoiler(object): 'logic': self.world.logic, 'mode': self.world.mode, 'retro': self.world.retro, - 'bomblogic': self.world.bomblogic, + 'bombbags': self.world.bombbags, 'weapons': self.world.swords, 'goal': self.world.goal, 'shuffle': self.world.shuffle, @@ -2113,7 +2113,7 @@ class Spoiler(object): outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") - outfile.write('Bomblogic: %s\n' % ('Yes' if self.metadata['bomblogic'][player] else 'No')) + outfile.write('Bombbags: %s\n' % ('Yes' if self.metadata['bombbags'][player] else 'No')) if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( diff --git a/CLI.py b/CLI.py index 911f03ab..0730eb03 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bomblogic', + 'bombbags', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -127,7 +127,7 @@ def parse_settings(): settings = { "lang": "en", "retro": False, - "bomblogic": False, + "bombbags": False, "mode": "open", "logic": "noglitches", "goal": "ganon", diff --git a/ItemList.py b/ItemList.py index e79392b8..f227856c 100644 --- a/ItemList.py +++ b/ItemList.py @@ -37,7 +37,7 @@ Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother', - 'retro', 'bomblogic', + 'retro', 'bombbags', 'extras', 'progressive_sword_limit', 'progressive_shield_limit', 'progressive_armor_limit', 'progressive_bottle_limit', 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) @@ -61,7 +61,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, - bomblogic = ['Bomb Upgrade (+10)'] * 2, + bombbags = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 4, progressive_shield_limit = 3, @@ -87,7 +87,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, - bomblogic = ['Bomb Upgrade (+10)'] * 2, + bombbags = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 3, progressive_shield_limit = 2, @@ -113,7 +113,7 @@ difficulties = { timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, - bomblogic = ['Bomb Upgrade (+10)'] * 2, + bombbags = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 2, progressive_shield_limit = 1, @@ -254,10 +254,10 @@ def generate_itempool(world, player): # set up item pool if world.custom: - (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bomblogic[player], world.customitemarray) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbags[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: - (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bomblogic[player], world.doorShuffle[player], world.logic[player]) + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbags[player], world.doorShuffle[player], world.logic[player]) if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] @@ -287,7 +287,7 @@ def generate_itempool(world, player): if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item not in possible_weapons: possible_weapons.append(item) - if not world.bomblogic[player] and item in ['Bombs (10)']: + if not world.bombbags[player] and item in ['Bombs (10)']: if item not in possible_weapons and world.doorShuffle[player] != 'crossed': possible_weapons.append(item) starting_weapon = random.choice(possible_weapons) @@ -318,7 +318,7 @@ def generate_itempool(world, player): p_item = next(item for item in items if item.name == potion and item.player == player) p_item.priority = True # don't beemize one of each potion - if world.bomblogic[player]: + if world.bombbags[player]: for item in items: if item.name == 'Bomb Upgrade (+10)' and item.player == player: item.advancement = True @@ -528,7 +528,7 @@ def set_up_shops(world, player): rss.locked = True cap_shop = world.get_region('Capacity Upgrade', player).shop cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro - if world.bomblogic[player]: + if world.bombbags[player]: if world.shopsanity[player]: removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] for remove in removals: @@ -536,7 +536,7 @@ def set_up_shops(world, player): world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade else: cap_shop = world.get_region('Capacity Upgrade', player).shop - cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bomblogic + cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbags def customize_shops(world, player): @@ -578,7 +578,7 @@ def customize_shops(world, player): shop.shopkeeper_config = shopkeeper # handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades if world.difficulty[player] == 'normal': - if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bomblogic[player]: + if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bombbags[player]: choices = [] for shop, idx, loc, item in possible_replacements: if item.name in ['Bombs (3)', 'Bombs (10)']: @@ -726,7 +726,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' 'Rupees (100)': 100, 'Rupees (300)': 300} -def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic): +def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbags, door_shuffle, logic): pool = [] placed_items = {} precollected_items = [] @@ -773,10 +773,10 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, diff = difficulties[difficulty] pool.extend(diff.baseitems) - if bomblogic: + if bombbags: pool = [item.replace('Bomb Upgrade (+5)','Rupees (5)') for item in pool] pool = [item.replace('Bomb Upgrade (+10)','Rupees (5)') for item in pool] - pool.extend(diff.bomblogic) + pool.extend(diff.bombbags) # expert+ difficulties produce the same contents for # all bottles, since only one bottle is available @@ -872,7 +872,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, pool.extend(['Small Key (Universal)']) return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bomblogic, customitemarray): +def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbags, customitemarray): if isinstance(customitemarray,dict) and 1 in customitemarray: customitemarray = customitemarray[1] pool = [] @@ -988,9 +988,9 @@ def test(): for shuffle in ['full', 'insanity_legacy']: for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: - for bomblogic in [True, False]: + for bombbags in [True, False]: for door_shuffle in ['basic', 'crossed', 'vanilla']: - out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bomblogic, door_shuffle, logic) + out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbags, door_shuffle, logic) count = len(out[0]) + len(out[1]) correct_count = total_items_to_place @@ -1000,7 +1000,7 @@ def test(): if retro: correct_count += 28 try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bomblogic)) + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbags)) except AssertionError as e: print(e) diff --git a/Main.py b/Main.py index df4426e8..2d49300b 100644 --- a/Main.py +++ b/Main.py @@ -79,7 +79,7 @@ def main(args, seed=None, fish=None): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() - world.bomblogic = args.bomblogic.copy() + world.bombbags = args.bombbags.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -384,7 +384,7 @@ def copy_world(world): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() - ret.bomblogic = world.bomblogic.copy() + ret.bombbags = world.bombbags.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() diff --git a/Mystery.py b/Mystery.py index 306b5c27..d02bd423 100644 --- a/Mystery.py +++ b/Mystery.py @@ -176,7 +176,7 @@ def roll_settings(weights): ret.retro = True ret.retro = get_choice('retro') == 'on' # this overrides world_state if used - ret.bomblogic = get_choice('bomblogic') == 'on' + ret.bombbags = get_choice('bombbags') == 'on' ret.hints = get_choice('hints') == 'on' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b6be375d..ca2f0e13 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,7 +10,7 @@ CLI: ```--shuffle_sfx``` When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable. -CLI: ```--bomblogic``` +CLI: ```--bombbags``` # Bug Fixes and Notes. @@ -18,7 +18,7 @@ CLI: ```--bomblogic``` * 0.5.0.2 * --shuffle_sfx option added * 0.5.0.1 - * --bomblogic option added + * --bombbags option added * 0.5.0.0 * Handles headered roms for enemizer (Thanks compiling) * Warning added for earlier version of python (Thanks compiling) diff --git a/Rom.py b/Rom.py index 8d86a16e..c2617aa0 100644 --- a/Rom.py +++ b/Rom.py @@ -1051,7 +1051,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x184000, [ # original_item, limit, replacement_item, filler 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees - 0x51, 0x00 if world.bomblogic[player] else 0x06, 0x31 if world.bomblogic[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bomblogic -> turns into Bombs (10) + 0x51, 0x00 if world.bombbags[player] else 0x06, 0x31 if world.bombbags[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbags -> turns into Bombs (10) 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade 0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 @@ -1188,7 +1188,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): equip[0x36C] = 0x18 equip[0x36D] = 0x18 equip[0x379] = 0x68 - if world.bomblogic[player]: + if world.bombbags[player]: starting_max_bombs = 0 else: starting_max_bombs = 10 @@ -1483,7 +1483,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bow_small = 70, 10 - elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bomblogic[player] else 'Bombs (10)']: + elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbags[player] else 'Bombs (10)']: rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) diff --git a/Rules.py b/Rules.py index 15847491..8252cfeb 100644 --- a/Rules.py +++ b/Rules.py @@ -1162,7 +1162,7 @@ def standard_rules(world, player): def bomb_escape_rule(): loc = world.get_location("Link's Uncle", player) - return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bomblogic[player] else 'Bombs (10)'] + return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bombbags[player] else 'Bombs (10)'] def standard_escape_rule(state): return state.can_kill_most_things(player) or bomb_escape_rule() diff --git a/mystery_example.yml b/mystery_example.yml index 028e7110..084e3e14 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -16,6 +16,9 @@ pot_shuffle: on: 1 off: 3 + bombbags: + on: 1 + off: 4 entrance_shuffle: none: 15 dungeonssimple: 3 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index f749e20a..7fa7a52c 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -224,7 +224,7 @@ "type": "bool", "help": "suppress" }, - "bomblogic": { + "bombbags": { "action": "store_true", "type": "bool" }, diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 3b96b6f8..eb83d4e9 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -264,7 +264,7 @@ "and a few other little things make this more like Zelda-1. (default: %(default)s)" ], "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"], - "bomblogic": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], + "bombbags": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ], "usestartinventory": [ "Toggle usage of Starting Inventory." ], "custom": [ "Not supported." ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index fe7fa9d6..54994697 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -192,7 +192,7 @@ "randomizer.item.hints": "Include Helpful Hints", "randomizer.item.retro": "Retro mode (universal keys)", "randomizer.item.pseudoboots": "Start with Pseudo Boots", - "randomizer.item.bomblogic": "Bomblogic", + "randomizer.item.bombbags": "Bombbags", "randomizer.item.worldstate": "World State", "randomizer.item.worldstate.standard": "Standard", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 15d049c8..e81f50d5 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -1,7 +1,7 @@ { "checkboxes": { "retro": { "type": "checkbox" }, - "bomblogic": { "type": "checkbox" }, + "bombbags": { "type": "checkbox" }, "shopsanity": { "type": "checkbox" }, "hints": { "type": "checkbox" diff --git a/source/classes/constants.py b/source/classes/constants.py index 90411547..86dc31bc 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -57,7 +57,7 @@ SETTINGSTOPROCESS = { "item": { "hints": "hints", "retro": "retro", - "bomblogic": "bomblogic", + "bombbags": "bombbags", "shopsanity": "shopsanity", "pseudoboots": "pseudoboots", "worldstate": "mode", From b3a3adb2f08ae1086ee7496589a783db401d19c4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 11 Aug 2021 06:05:46 -0500 Subject: [PATCH 032/104] Various music fixes MSU-1 LW2 track changes on Aga1 Kill instead of ped pull Fixed ambient SFX stutter issue --- Rom.py | 2 +- data/base2current.bps | Bin 141129 -> 141167 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index cab4cc48..97bdfc3f 100644 --- a/Rom.py +++ b/Rom.py @@ -31,7 +31,7 @@ from OverworldShuffle import default_flute_connections, flute_data JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'b71ccf874145fd21bf215b4b553e26ad' +RANDOMIZERBASEHASH = 'cc8fc59caa0bbe6d26ac64b9d2893709' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 290c305750aca233d584940f68f1a989a6cbb82e..6a4a34e265eac6877f69ae5e8469811901ebcee0 100644 GIT binary patch delta 6003 zcmW+)30xCL7td@G2!tcymdmiHfFMM{qd*aFsuUGG5L3moU_7w(Q?v@ZV8DcILKwn| zD<;4qM#R;oisFHyR)SSxwQ8;PsVn7 z8(7}Tl1JpJMQKURqe~R!F_zuhs?(1I}ea=#N2gQ1z6 zg8NX91%wNKWX~GCoh5b`$3#+%sn~T5Rh8;2UDVQY3SR`zV6z72_OMr>Z;!fbzga<3 z_%Y7BRHY7v>>$#ByBp1p8F~sg!&LtGBwa0?l(O}GrW=u7XTZ;HWgU`j2KB#>^UgzJFl z!FiU4D#7c5GGXo=KdUZ1V!*!8#R~4H<8}Tpt6(&Ju{0q(# z4G}4NSp`yBIjj`nNy7eZI2=p&sU7X+?G%2Z*8IqTudiht@>-SNKw~-{8at?lEvTjN z>>hR>g_H1>Xt;EifgNL3+$U{+%41hYA?H!{&IFjCYlt#ZSQtUaSm>A zi}M?cc2d@1U#inLP(fR&4)jp?7I?yKq>lsHKeOC`UqN*$A34NdP~biV`~@@J3zScP zVjX)7_;z% zHP(@j6m;(r+oQH?%r~yjiYF;t(qqoIs_MGW8 zfZjBuMoZzYRqW;nG-rXH-AUma^z1CwfKMmQjvj+T3m1uJf}f#P9Orov&Q%gBNANPoBOq}$JQT4A`!!np^8W{_CP(ZtYPtL8&k5jau!-Zc+D&* zqbwND;iPb;hV?*g^SW3+&F#|J8r9I$9mV2Z>ED#8TWZ-NB-%3Mb?Mt0@Fm;LcxMk= z7dBPg%7DvYuREH@4j7ZP&uhgBGnn@egme~uus%@ zZbP$lt~!a}JhQ6(hz9&G^M9VOU5)!SGQ0@phCz|i$U2VD!wM>j$|zg|A9>CJ(QvHS zfXwqo)^Xf8Uh?RX!;wmm>3hge*KH$b$}1>5x{EzR;j0>1MXma7Bb!uaz&j9|elMzN zWOLCefiU^h{VQ znGXt~Q*t^%rI=2;tSINJrcT{K-Iq6goRwH!k0mLteL`zK30J=8Po^w zOUH^@udyQJWi>FyC(652$I{a_9;J&sI*+JuEnMhBgLUw=k3xjvHGfYdKE^uVH$ECH zhMJTqi4!#JX06(VHp?_BbRNW??@`}+XwEF76zv+;Lq#5=mrwt7B4Knm@KYLg5R&(% zRgfCb_uX=l)!PlL?XLK7T4Pv)>i2Nc*gIl+_?jBk&Ifgg6JEfC^gz%Jr=-v4ZyxxM zwIy932#vqMS^)JY#z9i<6yhltStaW+Mkx!b#bd4KCVmURAZzht10NGMS;bR<(6i(R zmT<>Upi+SUYaKBC9^c2bW%O%Oxi@)vBk8rb49dQ$w|eAE^=A3e&sBTzBGn1TD z32)6`?5)dwRqtj8%7TJ|x&Xf3nx#160`je{OJ?x{{;;v_X9ax`gxtw2kyjkFxjIAe zw{Y$9K=9V8UM>;?v$baJej(ru;^b#5atbU$Lc(kupuOyC0ffNfM({2U2TndU4D1{Rly#YraBR!CjL-`# zo?HpKCA-aCbGEVfCHT%tUV@61tJp3Tn^nR7TEX6~VDD70L?!!sC9CAd5po-F0%f*#cQ*W9Y3XI^EoC_rhlGK zoa+Bo-I+O%atiQyeIjlF5`kwID9&7>*F(-S4oe7tFD$(tQM#5FYMI@W7$M7nJ$sjl zGWD$U?|eD~j%-^1{s(K@whoX){sYC~S||wDk{R%rQBYt(en>}r4TtZmjtrW4y(Vx8 zsV-elGQK19pAheq+z#Z45!_cZN@MtYySxqX!M+7pToRnvo`?+#g&W&*Fy0h+seRyN zNe@d~Elv?P`|lDvR-i1{l{uP$g_w@XF*PYKhg`7m++XJJ>GScT2R;v>pf`~r-^Q;xAXkLbedbu4}#*6&Zt5UxZ(}WFQU|#(}hl$pod8l^lIRYqC*gNKShSLdaWXMeC4ZCttmIH7f~>B z+?`dVq8tw7k^u+vxX{7ND3%@?-bZHz5iqEX6qzzZ3Dc%ES|fglHglECyin@I6UcC5 zFv&Rh6PE^Zpx=Qh{v&3(5{JG-SHr7zbALW2x-X-rz@h`+fRpgafw*iu@iDU-TA6QQ z7jv@e0tDl3*fNJ2Gbb2hKJx4?O$&A+n4C2be?k{myWSa(dx!a!L^jk&L^nM( zT_wX>Tghvt?W7x1Yn))@wEQAk>_Q#2ihGwf)5%uBdhm@NHbq!5J7<0aqn@krI_3?~( zsg#>S>*_}_<^m~~flwM_UMb~1LufK%z9r?RB9w*pE}h0rLvRjSSjv5l&}zo~M9O6$ zv>tKv;btIIf*AU6GZCsl41Kt4gmj3Z50?X_vSSw{RJPetY8G`^=^RUS-BYNjUY!$25HFp?>tvr)d{s{#U}yrc2z*$XOd+`DH-`qQ`rWAChnq_eD<{{PfcV-EGD-O9-QjJyiA zG-5nIWVMrwS=L$uS=0dft@u4uekTh6zLbgQOBsgx22*jg29BMsHF-^kjo&4@EGoQX zlB|F?zoW1iH&}S+6<7`z9?q72;$H^G_?M~XNSH7L1^%%6@atjzU%M0afM-{7m4rzW z>Q4N+P;_5@GY|j!G`s0tbbm}_SyqQpI$t{J8hqH148DL7M;>DQNzm=+M6d{E9gPKP zaMRIG!DD#jXt-D86nz6v)uU|bu?L))2p=5%I;f}dahtN;M=ZfCVw*=bX5mpjVx?&= zT+=xpWWx)cOM?fO>Qu)aJ4#`LW-2ahVpSe>!O7+H^qe>FvtywyM_9Ue*ju>bSn#|f z&88q*$hr092r4jwmiy{Rn<)zQ_#Ra4bZH$l3I2L)5$2r((~m1NVGFvC(gnN16Yn)1 z7g&?}IJmaq9zDVw!uC z`eus;dc+cR5qy0-0(=GsoRIshMu%VQ2p2*{1m2vFlb!5US%ImhrTMT z7-V73;RnLHhV<9f{$0_imL2fy8Va|ccnvng>XRc&v-h@j%j>@R1fEuAxAlK>5Sb9w z@1w`4T6dC<7!&!dO;35X4{GO(DqoJPCljncwol`-+dQq+hqBw~L#9{8O9(W7tAC@K zc$l`dgmq-M*%9sU=n@j-b!GuU-bOs$bx4kIo}}vO(XM>LR7gQ&^qwo~lc8cvi z#a=kg-aE}6j@qe4xgNdikW%L8UfQ$N)*n-LF&LBWUq7yiDmJVlKi_TZYCX+L zVr@>l;GvY5WP5HG%B*LJB-;xFcEjhFM+6n}z+r34c!izA6$hWu_>OAkHV#c-(Q3>#FqHUF_5 zlk!?@G}V3%)?P7Ue)C}L)yM>}@p(;(qG~3=-_7ef(9G`UH@HX;{PsU6>WAO`2fg~? zVYuaLDCQS$Wv=eWz#2I9x-0-6r6Y&YzFyTW6JD0s-MC>dOOokiNL&xY5`&@XdWu)@ zsOodmUX}#wQGuJ_qw8_eDTTUwl{~v&be?$--t&W*;}OwzzAVAEi2%{9sf9XynUGM* z5@70$c;ETUbZ4wBwn286ygmIx7=Z`eaw9~zWQvYXgAF%^mu@N4k;!zKp+Jc*I>N{c zb%uc%H@PV0n_OL0XVtN){iikFQ7=pMh~5{XEKdlhn{M z#6iK$3Qz*8ZVm@O!4o%YOFvoZO8lg_=oD5>YqPjE%%3K)v->Py7rgdQu!J3P4ncyhI75?JdFDiy}!J;G?d>8 zQ5rWm$x!pz0H(3d4DiiFQ?-|AMy%dgEx=zwvvok>Ykwk7w2wCnU)pYt-lS|XL;Ek; zfz$i;Y?aH#p>nwLrY5LR#@lP=$!5Y&ZjE#w*B7+xzUsbGHXRn<$^fnK^sUmtk}GU` z9I%O;QZBemZWnQFGfHhA?EK7_%lk3e-JD@ge&z>UW6L~1J}crd$N$p8>`#zjJ+Xux^$tE z_?ua+y{wM|ybSy{ynFAsU%_?O>C!8Cts#0W1Qij{EvL7@1NZ&VtNh&k<{&)bhmKdz zPN}=_Z!WBox)L2DB(4PQ<`0SAg1~B6|64qmXYKjzG6?VxR2!d}LwVd4=+}As3+&0FCz;O61FiBgpNUCF1@Qlv!9sOdYP;-8W6=mjFX0_img zRTdG?spKNj&G3YNy?6$JlKXWN)%84JkK)q)Zd*#v_T7siPP-) zk`r>L%tGsTpD$_4|L~DUw-E6Uat1; z+FX~hav#*Zc^dSrhrLPjyAFz{?(I5&qSb_ocWa0kSpC)?xL9|;9q1a7=l%ob#pAgi z6Hou)!p3h7(*kVU6mrveH=--?8*KKL6Ng9wZ9rad}t2*D4Y z?vWt|=?3F`FT&CSBX4k{dbfnh^EJIDxZvJjiG Tk+u!NibXGm6xi4~8yEZ^T{o(= delta 6013 zcmW+)30xD$_s`^ja3_F(0ut5(@IXNE08~T-R7CMAV%p*jjR&4p^as0vfB`lk3}M9u zvxJQp4p&VT@j_55K`XJg6_0wgwl=mk9<5gUZ~sG+&ztw1o%_vuZ|2R58)EAXaTONx zu8QStT=k5&_>5o^a8f=GZ#+;ZKX{9^w~rO$#MqoNo!o#o+H=cPI7lxh<-xk8k%3RU zScy_)Iw#bS*s1SOLml=`!`j7Xh$dY_mggHaYqw_|C1g>hdU+WXfN)IN%Ld>i_OOy= zNsK+p)TWJl9(CLt#dj0&0B>sS)jqfYj0Jgc4;U9Nva(`akLh)&oA3-=2gH8BIiOl6 zUuk6R10?2sj}0$Zzp}!wz$o#i8g}Y0Vr)2k4?Y@pq8v3rzsn?ckYwG_h|M+3$Pzuq zhYJ1(a0P1le#4*LWH0Nz9ECO~dorPZTEgyVs3=osXu{SOlUOpm#9ulpzL)(5`t+*1 zjxutJ#LjZeT9x|$s@ai*4s+Er_C;C}D~1Vzq)A(As7di9AJSd$#9AHJT*BHV`*ql~ z{VjI6T8|AWVev6u_*H7iKjKp20#qFOmBbq02|=9KmR|N(6_)$&3|y#GV^a7*FrELj z1jY%YeLk*6tBC5?V_jwhatJlH4i*Z>0S#;w&IEG!To?@+V6bR}b6GWeUx&rPRM8wT z!a|C60P)qUEDxM^9;)hG z&F0pSSZpubMq+E>_u^>j6dgO2QDZ-A&xS`UtLm`5FvKO@r=yRpr#37rO-GF)_qQA% z6uG7X8(iR; ztGM|sYd@mHzNlgBKkBe6nlSr;#+e5h`7ty*ySS8;f4Ily6zehWDPy;{(P+E=_=yR^ zb@EZ)vGyD!pUXGcUbU^98MwVPk{~fjFOy?YVZn4ru0D&XlYdk>xOU$%62qa)ZD>Sh z1#3S>6^yQ~Br$tAyDt>AnWbgxNi18-E@gGtLV~gP>f~GCYPU>q4u0V_&f^&TNP(;D zRXa-bau4{M+se^9zh?KTN$i_;W>5+h)S)*&5B+~0t$Ily2Iy=-l8>cQtjmRGCi z>~E1HX-VLh?iOTF1s)-bQft`V$hiN!zy=f(ib;feEHk)WA=T`!YAn8?N&1m`63%&K zRQci!n2q^w+YhL)x71Y8VU^uJppy@8WbLP^HR-y1C5df{^#lkdD?vJZA?c1* z%VVm2@F`xTphTR~PCkh;{=0NyaUJI0&!%6!Btui(*v~Q~wx*wzBVm5H0Ut_dip#%a z#YnGq!qMJgUaK`MHGfYhmG6G$v&Qj|aexHo%?RKoGoE?c)eBJ^QA_YD< zU$I)7ZnMo9Gf?HaEr>sWNi!dgsGY-qsjC06cJEAly0U(NUw?;R{|i5x;bGriXA0iu zDlijFPFyL-n()M8OcV-(q}Nz8Ab;U=kce`Ku;E{^3f6t9LKaYid0KjsPXOR-*_omf z@SinW#Pfj2W7TyQcf}Z#3bBV4zlDzl-iPF&DnikcF`FhlTa<7?@_mcTl4JpBvt%qU z7Kth8ZU;SM{9o|S%GDCHuZgj`*nlE8H@7x=LA7O`{InC8Z4srvu-D9*pCvF~+Zz?d;(`sYEnTeKqUEupAQ4}!}~v(oc>!V_gDmJ>5y;#b2m$E&j?3yyRw~SSAv+yV@Z~#S5lu3H_xrB zvtdn@Yz{~73DLgH!7mQxs?MbI;c>GMNPt(&v-mrQKygc-yQ{|ZN-eW_;4!kv@IlKu z@dPdFc%4Ie!uhSAfOy!_S~7Gw_9x5?u7vY~D~U9CRxivoBc-D@Y=fz7RUmdmz-dFWHciQl^Xe zpx`txwz^JYTImp)Rx}4y!~G^jnkmI5#9m)zDT~=5=9axKwDEHlxrfrH&~O4@Lk|~d zI^F;K>=gdq%;PD--&0b#olwv*D0P8Kn3r9t88#g^?8}C8kIoG~Wn|@T1{RA^ve-2k zh7CWPM8MD9!O{7on(uI;b|z_IL6TMt z9AVTdgk3KZfz6(q@PpfYG^#Ddh6FJQ^AaC!BIN5~EEfl=VLlf$Dve~RF~j?*i~t-4 zC<(D4JqR}>ZmHB`Cn<(2qeFto^ZoERHFOhlZ@o?i!!#6~5b2HItI6%12HTuzdu2PUsWs1m7o15{M zgUqAufgY{KgR%1F66P}5*^p_v>hmqP8v z>!dWZR?4MPn!2GhlPl%Y5DKN4jZ$tdLJ>4`U&_rxD4J#-NxAt5O{1A-Qf>i4nP_Aw zmx0g<)M1e~w+O*is6%fq6QMlRp*Ob}p{=MxZ*B>c5}nK|2`5LhU#(}hNx6^FT*+5b z?h~3+dvl+{QWduX&Q?`iByZBpJMHgkd{$gjObLl(I8nR*KRJxjDv8HP9{z~`GANg( zUsPPSp;1@#l4f>G(e|9CzBAk={xS@ZE=EvKX;s`x!v>smsb}6bXW)d?a8B)F97^j{ zoWd}jAVce!-Ef%_`W6!r1{ncE2qGGBRA(C9ZsyX2gS(0Kv@wQHJ(T)9$#C+Ww;{UZ z}o2N(xGKebp|c*@RQx3B zZZ`H^H(T&Ne9%}j%$tKrjSp4uE&Qh=4kW^9r+?zlO@^VJ$sif7?;Hz+u)1?PxB+dQ z!#(p-v<*B}ufo)8^Sk5<|L)ulT4DYfKjiz%&a4Ch@P{*Nhbam*s&n>(MX;fK9`>w} zRk_y=iz}`T9aRihoee6YS!(i~1^6Sp>NUeN^6sAP8lsGP9}&WHb#=`?(n&WN0<49d z4NZn&h#8s;S%@_@8M3W~1=Pn_wW;^f&?@T_nar<=`fOg~hdpI5(^~`6J}ZYyJ}a-w zA^k#0|4?d|w}!A9HlY%q0g+>q?^YrYiw$RCgPmsM`v8> zYE<7fl|%QjI5inc&W8do7;`>q;iTDGxHZ}dSKsBrKvdy`jBUOJM&f)m}XTJ`rGuPv17W@lHO zArDvo*7?B~Vub!pXKX9~%+(Uz#xpsbfZAi()pZ(x;g$*Ar-1Lr{^UtFGXUCgSfO?? zR>%cX0hYgd)(ZI>mRml(8Y4g&R^7LqzhN?TzBU)cz|3nKU3N})sB^gNTSCj1*EaIO zLOAipDE@EfVfKwAand<1;7y?{9I`hu!ot$Y1}yIa`$WcVfumFeW{@u?RgbDT;HRN? z4bc$m4Ne3c1&8)e0(W6X|1jUEC9XJ;)H4SiX?@g^7+fz%zYh2H2l<8Oi}B9u=exZA z@rd~Mg*N4CVV9JW!K3}ZgYmGzR^&5Urh)S-8;MZ!I#L;GE}|95aKg<|S-WOy>soV- zJYKn?b_>(F>msYU$hKc(k9M<{yV+9-#cFg%r1mC0IH{0*)w8yDt##1aY;JH9q322R z`y_w01ZvIGFPZ*~kT?V~^<`?$)Z;g0wXpT(h{Rm%=LVs}$7Bu2bGg(+h_&ssX#4YK z;r8d%VY_vkhy{nOUFF@ZWUSR;6aFN{=U89bM6%k;_$2Eq1k}*;>nWo6%QD9$IPdE@ zz9l_ZPKcbSL*;VgvF@a-M7XQl805k?lv|@mqR+b{0(gX2gWl-7l--^Frr7jFEU z!Z?Zt8*T^l_Y8&SZpVA(#8zFI|E6%57A2SxiU!7ogFMaSGM>#hJewJb^aDsJ~vmP-SoPUE?5jh z@A!tqPN{;z=~eB%J)tHZa->^Y-V`?f4nNM?j9#!5rjCW7cT15M zK5{o2oQ8Mr))W;fobhkvUpYj`cbJ_U7J13yZ(u*a87|vZ<1sr5kG*fbGMLRh|aF zv08nV5PJg|%cr?-hv3=bHr`TfZ95ZQpfEA;w|k5IWBQMjM9D^?G+J@DJRncTYhie@ zNpSi7Sl5yL0qdTqo+xB7P=7xSl*0%2i}&PxPTT zP81-&m}^eJC+6Z&Y1CK)y#2i%)66k7KDC)Y^hoM?Nrt=-DSo^@F|<7~)b_zlkh~;n zIMhDy3kH*1@aDz1PJ}`saqZUdh9(IQuU+zwsz-7)0rost#$VzJhdvAfJUIE`O>hE= zA8i6^xa-mXKs0>wXc;iWd5?n;&V4*5U`V73Zsy^jT+3Tpq;Uh3EYt;uAMb=$9?JsV z_f)k%oH-EXaOy%S{+ixgc~d(M@Y1mE>L=%X=ig=>PJNQM<#?|dCqqMfqNv%>_QV&( z=}(_D1>{Cv@A%`zMRgap_u53MGv2Y=%NeH@`a6_|2A0sXRHef zIb^pW6%hry|MZQFoTIMe0b3Y1<8|v=>fxwQ)TEbLG}2I`<;cT>%|0~2fROWh}^yKlJ&t_&^zqV)ZDN4xSpGp)Yx!_%!_ErYsZj?I!nA6Y* zjpE0Mj!6JG0pLl+^TDWLo>mq;kXSj#OY2C-Z|@Dur@T<-#wn z8+bwGbhyL;1@J1E(4vy@YL~B6sY0cfvYLzIK>*l7l`H{az`vty3HZ?G9=`q_@p)4* pv9D= Date: Wed, 11 Aug 2021 06:11:41 -0500 Subject: [PATCH 033/104] Version bump 0.1.7.3 --- CHANGELOG.md | 7 +++++++ OverworldShuffle.py | 2 +- README.md | 3 +-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94736e42..3ff444f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 0.1.7.3 +- Fixed minor issue with ambient SFX stopping and starting on OW screen load +- MSU-1 changed to play LW2 (track 60) when Aga1 is killed instead of ped pull +- Added dynamic flute exits for all LW OW regions +- Improved spoiler log playthru pathing accuracy by including flute routing +- Fixed issue with generating a filename for vanilla OW settings + ### 0.1.7.2 - Fixed music algorithm to play correct track in OW Shuffle - Removed convenient portal on WDM in OW Layout Shuffle diff --git a/OverworldShuffle.py b/OverworldShuffle.py index e80713ae..6aa22ece 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.7.2-u' +__version__ = '0.1.7.3-u' def link_overworld(world, player): # setup mandatory connections diff --git a/README.md b/README.md index 2d8221f3..3ecdb94f 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,8 @@ This is a very new mode of LTTPR so the tools and info is very limited. ### If you want to playtest this, know these things: - Big Red Bomb may require bomb duping as ledge drops may be in the way of your path to the Pyramid Fairy crack -- Do NOT grab the Frogsmith until you have seen the Blacksmith location. Doing so may prevent you from continuing in your save file. - If you fake flipper, beware of transitioning south. You could end up at the top of the waterfall in the southeast of either world. If you mistakenly drop down, it is important to NOT make any other movements and S+Q immediately when the game allows you to (might take several seconds, the game has to scroll back to the original point of water entry) or there will be a hardlock. Falling from the waterfall is avoidable but it is super easy to do as it is super close to the transition. -- In Crossed OW Tile Swap, there are some interesting bunny water-walk situations that can occur, these are mean to be out-of-logic but beware of logic bugs around this area. +- In Crossed OW Tile Swap, there are some interesting bunny water-walk situations that can occur, these are meant to be out-of-logic but beware of logic bugs around this area. ### Known bugs: - ~~In Mixed OW Tile Swap, Smith and Stumpy have issues when their tiles are swapped. Progression cannot be found on them when these tiles are swapped~~ (Fixed in 0.1.6.4) From dcba2be7574c5bb6dd0bd7f4da33f8c8863db439 Mon Sep 17 00:00:00 2001 From: StructuralMike <66819228+StructuralMike@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:29:12 +0200 Subject: [PATCH 034/104] Bombbag instead of Bombbags --- BaseClasses.py | 8 ++--- CLI.py | 4 +-- ItemList.py | 36 +++++++++---------- Main.py | 4 +-- Mystery.py | 2 +- RELEASENOTES.md | 4 +-- Rom.py | 6 ++-- Rules.py | 2 +- mystery_example.yml | 2 +- resources/app/cli/args.json | 2 +- resources/app/cli/lang/en.json | 2 +- resources/app/gui/lang/en.json | 2 +- resources/app/gui/randomize/item/widgets.json | 2 +- source/classes/constants.py | 2 +- 14 files changed, 39 insertions(+), 39 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index e8e471c9..69669a33 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -114,7 +114,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', False) set_player_attr('bigkeyshuffle', False) - set_player_attr('bombbags', False) + set_player_attr('bombbag', False) set_player_attr('difficulty_requirements', None) set_player_attr('boss_shuffle', 'none') set_player_attr('enemy_shuffle', 'none') @@ -687,7 +687,7 @@ class CollectionState(object): # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - return (not self.world.bombbags[player] or self.has('Bomb Upgrade (+10)', player)) + return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -2014,7 +2014,7 @@ class Spoiler(object): 'logic': self.world.logic, 'mode': self.world.mode, 'retro': self.world.retro, - 'bombbags': self.world.bombbags, + 'bombbag': self.world.bombbag, 'weapons': self.world.swords, 'goal': self.world.goal, 'shuffle': self.world.shuffle, @@ -2113,7 +2113,7 @@ class Spoiler(object): outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") - outfile.write('Bombbags: %s\n' % ('Yes' if self.metadata['bombbags'][player] else 'No')) + outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No')) if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( diff --git a/CLI.py b/CLI.py index 0730eb03..bb9ab0a2 100644 --- a/CLI.py +++ b/CLI.py @@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', - 'bombbags', + 'bombbag', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', @@ -127,7 +127,7 @@ def parse_settings(): settings = { "lang": "en", "retro": False, - "bombbags": False, + "bombbag": False, "mode": "open", "logic": "noglitches", "goal": "ganon", diff --git a/ItemList.py b/ItemList.py index f227856c..2edad3fe 100644 --- a/ItemList.py +++ b/ItemList.py @@ -37,7 +37,7 @@ Difficulty = namedtuple('Difficulty', ['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield', 'basicshield', 'progressivearmor', 'basicarmor', 'swordless', 'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother', - 'retro', 'bombbags', + 'retro', 'bombbag', 'extras', 'progressive_sword_limit', 'progressive_shield_limit', 'progressive_armor_limit', 'progressive_bottle_limit', 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) @@ -61,7 +61,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10, - bombbags = ['Bomb Upgrade (+10)'] * 2, + bombbag = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 4, progressive_shield_limit = 3, @@ -87,7 +87,7 @@ difficulties = { timedohko = ['Green Clock'] * 25, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, - bombbags = ['Bomb Upgrade (+10)'] * 2, + bombbag = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 3, progressive_shield_limit = 2, @@ -113,7 +113,7 @@ difficulties = { timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5, timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10, retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15, - bombbags = ['Bomb Upgrade (+10)'] * 2, + bombbag = ['Bomb Upgrade (+10)'] * 2, extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra], progressive_sword_limit = 2, progressive_shield_limit = 1, @@ -254,10 +254,10 @@ def generate_itempool(world, player): # set up item pool if world.custom: - (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbags[player], world.customitemarray) + (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: - (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbags[player], world.doorShuffle[player], world.logic[player]) + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player]) if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] @@ -287,7 +287,7 @@ def generate_itempool(world, player): if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']: if item not in possible_weapons: possible_weapons.append(item) - if not world.bombbags[player] and item in ['Bombs (10)']: + if not world.bombbag[player] and item in ['Bombs (10)']: if item not in possible_weapons and world.doorShuffle[player] != 'crossed': possible_weapons.append(item) starting_weapon = random.choice(possible_weapons) @@ -318,7 +318,7 @@ def generate_itempool(world, player): p_item = next(item for item in items if item.name == potion and item.player == player) p_item.priority = True # don't beemize one of each potion - if world.bombbags[player]: + if world.bombbag[player]: for item in items: if item.name == 'Bomb Upgrade (+10)' and item.player == player: item.advancement = True @@ -528,7 +528,7 @@ def set_up_shops(world, player): rss.locked = True cap_shop = world.get_region('Capacity Upgrade', player).shop cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro - if world.bombbags[player]: + if world.bombbag[player]: if world.shopsanity[player]: removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] for remove in removals: @@ -536,7 +536,7 @@ def set_up_shops(world, player): world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade else: cap_shop = world.get_region('Capacity Upgrade', player).shop - cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbags + cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbag def customize_shops(world, player): @@ -578,7 +578,7 @@ def customize_shops(world, player): shop.shopkeeper_config = shopkeeper # handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades if world.difficulty[player] == 'normal': - if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bombbags[player]: + if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bombbag[player]: choices = [] for shop, idx, loc, item in possible_replacements: if item.name in ['Bombs (3)', 'Bombs (10)']: @@ -726,7 +726,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' 'Rupees (100)': 100, 'Rupees (300)': 300} -def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbags, door_shuffle, logic): +def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic): pool = [] placed_items = {} precollected_items = [] @@ -773,10 +773,10 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, diff = difficulties[difficulty] pool.extend(diff.baseitems) - if bombbags: + if bombbag: pool = [item.replace('Bomb Upgrade (+5)','Rupees (5)') for item in pool] pool = [item.replace('Bomb Upgrade (+10)','Rupees (5)') for item in pool] - pool.extend(diff.bombbags) + pool.extend(diff.bombbag) # expert+ difficulties produce the same contents for # all bottles, since only one bottle is available @@ -872,7 +872,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, pool.extend(['Small Key (Universal)']) return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbags, customitemarray): +def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray): if isinstance(customitemarray,dict) and 1 in customitemarray: customitemarray = customitemarray[1] pool = [] @@ -988,9 +988,9 @@ def test(): for shuffle in ['full', 'insanity_legacy']: for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: - for bombbags in [True, False]: + for bombbag in [True, False]: for door_shuffle in ['basic', 'crossed', 'vanilla']: - out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbags, door_shuffle, logic) + out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) count = len(out[0]) + len(out[1]) correct_count = total_items_to_place @@ -1000,7 +1000,7 @@ def test(): if retro: correct_count += 28 try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbags)) + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) except AssertionError as e: print(e) diff --git a/Main.py b/Main.py index 2d49300b..60ff9a6b 100644 --- a/Main.py +++ b/Main.py @@ -79,7 +79,7 @@ def main(args, seed=None, fish=None): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() - world.bombbags = args.bombbags.copy() + world.bombbag = args.bombbag.copy() world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} world.crystals_ganon_orig = args.crystals_ganon.copy() @@ -384,7 +384,7 @@ def copy_world(world): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() - ret.bombbags = world.bombbags.copy() + ret.bombbag = world.bombbag.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() diff --git a/Mystery.py b/Mystery.py index d02bd423..e2095e4a 100644 --- a/Mystery.py +++ b/Mystery.py @@ -176,7 +176,7 @@ def roll_settings(weights): ret.retro = True ret.retro = get_choice('retro') == 'on' # this overrides world_state if used - ret.bombbags = get_choice('bombbags') == 'on' + ret.bombbag = get_choice('bombbag') == 'on' ret.hints = get_choice('hints') == 'on' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ca2f0e13..76783862 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,7 +10,7 @@ CLI: ```--shuffle_sfx``` When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable. -CLI: ```--bombbags``` +CLI: ```--bombbag``` # Bug Fixes and Notes. @@ -18,7 +18,7 @@ CLI: ```--bombbags``` * 0.5.0.2 * --shuffle_sfx option added * 0.5.0.1 - * --bombbags option added + * --bombbag option added * 0.5.0.0 * Handles headered roms for enemizer (Thanks compiling) * Warning added for earlier version of python (Thanks compiling) diff --git a/Rom.py b/Rom.py index c2617aa0..063cab58 100644 --- a/Rom.py +++ b/Rom.py @@ -1051,7 +1051,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x184000, [ # original_item, limit, replacement_item, filler 0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees - 0x51, 0x00 if world.bombbags[player] else 0x06, 0x31 if world.bombbags[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbags -> turns into Bombs (10) + 0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10) 0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade 0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode) 0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20 @@ -1188,7 +1188,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): equip[0x36C] = 0x18 equip[0x36D] = 0x18 equip[0x379] = 0x68 - if world.bombbags[player]: + if world.bombbag[player]: starting_max_bombs = 0 else: starting_max_bombs = 10 @@ -1483,7 +1483,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bow_small = 70, 10 - elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbags[player] else 'Bombs (10)']: + elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) diff --git a/Rules.py b/Rules.py index 8252cfeb..23a15f67 100644 --- a/Rules.py +++ b/Rules.py @@ -1162,7 +1162,7 @@ def standard_rules(world, player): def bomb_escape_rule(): loc = world.get_location("Link's Uncle", player) - return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bombbags[player] else 'Bombs (10)'] + return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)'] def standard_escape_rule(state): return state.can_kill_most_things(player) or bomb_escape_rule() diff --git a/mystery_example.yml b/mystery_example.yml index 084e3e14..065fefc1 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -16,7 +16,7 @@ pot_shuffle: on: 1 off: 3 - bombbags: + bombbag: on: 1 off: 4 entrance_shuffle: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 7fa7a52c..a0113222 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -224,7 +224,7 @@ "type": "bool", "help": "suppress" }, - "bombbags": { + "bombbag": { "action": "store_true", "type": "bool" }, diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index eb83d4e9..3ace31f7 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -264,7 +264,7 @@ "and a few other little things make this more like Zelda-1. (default: %(default)s)" ], "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"], - "bombbags": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], + "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ], "usestartinventory": [ "Toggle usage of Starting Inventory." ], "custom": [ "Not supported." ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 54994697..c4cd8a11 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -192,7 +192,7 @@ "randomizer.item.hints": "Include Helpful Hints", "randomizer.item.retro": "Retro mode (universal keys)", "randomizer.item.pseudoboots": "Start with Pseudo Boots", - "randomizer.item.bombbags": "Bombbags", + "randomizer.item.bombbag": "Bombbag", "randomizer.item.worldstate": "World State", "randomizer.item.worldstate.standard": "Standard", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index e81f50d5..038e668c 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -1,7 +1,7 @@ { "checkboxes": { "retro": { "type": "checkbox" }, - "bombbags": { "type": "checkbox" }, + "bombbag": { "type": "checkbox" }, "shopsanity": { "type": "checkbox" }, "hints": { "type": "checkbox" diff --git a/source/classes/constants.py b/source/classes/constants.py index 86dc31bc..b184643b 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -57,7 +57,7 @@ SETTINGSTOPROCESS = { "item": { "hints": "hints", "retro": "retro", - "bombbags": "bombbags", + "bombbag": "bombbag", "shopsanity": "shopsanity", "pseudoboots": "pseudoboots", "worldstate": "mode", From 2f957df3fa04f07a3383440b332dd0a43bdb0f57 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 11 Aug 2021 20:59:44 -0500 Subject: [PATCH 035/104] Fixed issue with Swapped Ganon Hole not being accessible --- EntranceShuffle.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 495a99be..2d06b423 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2863,8 +2863,12 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), ('Mimic Cave', 'Mimic Cave'), - ('Pyramid Exit', 'Pyramid Exit Ledge') - ] + ('Pyramid Exit', 'Pyramid Exit Ledge'), + ('Pyramid Hole', 'Pyramid'), + ('Pyramid Entrance', 'Bottom of Pyramid'), + ('Inverted Pyramid Hole', 'Pyramid'), + ('Inverted Pyramid Entrance', 'Bottom of Pyramid') + ] swapped_connections = { 0x03: [ @@ -2883,18 +2887,14 @@ swapped_connections = { ] } -open_default_connections = [('Links House', 'Links House'), +open_default_connections = [('Links House', 'Links House'), ('Links House Exit', 'Links House Area'), - ('Big Bomb Shop', 'Big Bomb Shop'), - ('Pyramid Hole', 'Pyramid'), - ('Pyramid Entrance', 'Bottom of Pyramid') + ('Big Bomb Shop', 'Big Bomb Shop') ] -inverted_default_connections = [('Links House', 'Big Bomb Shop'), - ('Links House Exit', 'Big Bomb Shop Area'), - ('Big Bomb Shop', 'Links House'), - ('Inverted Pyramid Hole', 'Pyramid'), - ('Inverted Pyramid Entrance', 'Bottom of Pyramid') +inverted_default_connections = [('Links House', 'Big Bomb Shop'), + ('Links House Exit', 'Big Bomb Shop Area'), + ('Big Bomb Shop', 'Links House') ] # non shuffled dungeons @@ -2950,19 +2950,19 @@ default_dungeon_connections = [('Desert Palace Entrance (South)', 'Desert South ('Dark Death Mountain Ledge (East)', 'Turtle Rock Chest Portal'), ('Turtle Rock Isolated Ledge Exit', 'Dark Death Mountain Isolated Ledge'), ('Turtle Rock Isolated Ledge Entrance', 'Turtle Rock Eye Bridge Portal') - ] + ] open_default_dungeon_connections = [('Ganons Tower', 'Ganons Tower Portal'), ('Ganons Tower Exit', 'West Dark Death Mountain (Top)'), ('Agahnims Tower', 'Agahnims Tower Portal'), ('Agahnims Tower Exit', 'Hyrule Castle Ledge') - ] + ] inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Portal'), ('Ganons Tower Exit', 'Hyrule Castle Ledge'), ('Agahnims Tower', 'Ganons Tower Portal'), ('Agahnims Tower Exit', 'West Dark Death Mountain (Top)') - ] + ] indirect_connections = { 'Turtle Rock Ledge': 'Turtle Rock', From 7623de396c20063ce1a39306ef66dbd39d322f25 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 13:39:32 -0600 Subject: [PATCH 036/104] Fixed some bk special handling --- DungeonGenerator.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5174762e..3c64df89 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player): proposed_map = {} doors_to_connect = {} all_regions = set() - bk_needed = False bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) - bk_special = bk_special or check_for_special(sector) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) paths = determine_paths_for_dungeon(world, player, all_regions, builder.name) dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions, proposed_map, doors_to_connect, bk_needed, bk_special, world, player) @@ -106,14 +107,15 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() - bk_needed = False bk_special = False for sector in builder.sectors: for door in sector.outstanding_doors: doors_to_connect[door.name] = door all_regions.update(sector.regions) - bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player) - bk_special = bk_special or check_for_special(sector) + bk_special |= check_for_special(sector.regions) + bk_needed = False + for sector in builder.sectors: + bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player) proposed_map = {} choices_master = [[]] depth = 0 @@ -187,8 +189,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon return proposed_map -def determine_if_bk_needed(sector, split_dungeon, world, player): - if not split_dungeon: +def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player): + if not split_dungeon or bk_special: for region in sector.regions: for ext in region.exits: door = world.check_for_door(ext.name, player) @@ -197,8 +199,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player): return False -def check_for_special(sector): - for region in sector.regions: +def check_for_special(regions): + for region in regions: for loc in region.locations: if loc.forced_big_key(): return True @@ -417,6 +419,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a # origin has no more hooks, but not all doors have been proposed if not world.bigkeyshuffle[player]: possible_bks = len(dungeon['Origin'].possible_bk_locations) + if bk_special and check_for_special(dungeon['Origin'].visited_regions): + possible_bks = 1 true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed] if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect): return False @@ -450,7 +454,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special) for piece in dungeon.values(): all_visited.update(piece.visited_regions) - if not bk_possible and len(piece.possible_bk_locations) > 0: + if ((not bk_possible and len(piece.possible_bk_locations) > 0) or + (bk_special and check_for_special(piece.visited_regions))): bk_possible = True if len(all_regions.difference(all_visited)) > 0: return False From 9153dbd38268cc62008f9e775312698fd7b4c766 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 13:44:47 -0600 Subject: [PATCH 037/104] Fixed a problem that cropped up in retro+vanilla with big key placement --- Dungeons.py | 7 +++++++ KeyDoorShuffle.py | 7 ++++--- Main.py | 2 +- RELEASENOTES.md | 4 ++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Dungeons.py b/Dungeons.py index 596da920..81fc4941 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,6 +375,13 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } +default_key_counts = { + 'Hyrule Castle': 1, 'Eastern Palace': 0, 'Desert Palace': 1, + 'Tower of Hera': 1, 'Agahnims Tower': 2, 'Palace of Darkness': 6, + 'Swamp Palace': 1, 'Skull Woods': 3, 'Thieves Town': 1, + 'Ice Palace': 2, 'Misery Mire': 3, 'Turtle Rock': 4, 'Ganons Tower': 4 +} + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 549a739c..3ce9132f 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs +from Dungeons import dungeon_keys, dungeon_bigs, default_key_counts from DungeonGenerator import ExplorationState, special_big_key_doors @@ -1384,9 +1384,10 @@ def create_key_counters(key_layout, world, player): flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) if world.doorShuffle[player] == 'vanilla': - state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys) + state.key_locations = default_key_counts[key_layout.sector.name] else: - state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num + builder = world.dungeon_layouts[player][key_layout.sector.name] + state.key_locations = builder.key_doors_num - builder.key_drop_cnt state.big_key_special, special_region = False, None for region in key_layout.sector.regions: for location in region.locations: diff --git a/Main.py b/Main.py index df4426e8..eb36681f 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.0.2-u' +__version__ = '0.5.0.3-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b6be375d..3b414321 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,10 @@ CLI: ```--bomblogic``` # Bug Fixes and Notes. +* 0.5.0.3 + * Fixed a bug in retro+vanilla and big key placement + * Fixed a problem with shops not registering in the Multiclient until you visit one + * Fixed a bug in the Mystery code with sfx * 0.5.0.2 * --shuffle_sfx option added * 0.5.0.1 From d6a56e645ecc741e782f1533ff7d0e0259b27587 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 13:45:20 -0600 Subject: [PATCH 038/104] Bug with MutliClient that didn't register shop locations unless you were in one --- MultiClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiClient.py b/MultiClient.py index bf64a2d7..501630e0 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -846,7 +846,7 @@ async def track_locations(ctx : Context, roomid, roomdata): new_locations.append(Regions.lookup_name_to_id[location]) try: - if roomid in location_shop_ids: + if ctx.shop_mode or ctx.retro_mode: misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN) for cnt, b in enumerate(misc_data): my_check = Regions.shop_table_by_location_id[0x400000 + cnt] From de6f3683d0ae9b7b9ac0bea7a8f95d1743a94326 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 13:45:51 -0600 Subject: [PATCH 039/104] Fixed bug with sfx_shuffle in mystery --- Mystery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index 306b5c27..c0666e6a 100644 --- a/Mystery.py +++ b/Mystery.py @@ -234,7 +234,7 @@ def roll_settings(weights): ret.heartbeep = get_choice('heartbeep', romweights) ret.ow_palettes = get_choice('ow_palettes', romweights) ret.uw_palettes = get_choice('uw_palettes', romweights) - ret.uw_palettes = get_choice('shuffle_sfx', romweights) == 'on' + ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on' return ret From fd24c016e253dcb02efe1c654b438d56a30282b2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 15:59:01 -0600 Subject: [PATCH 040/104] Total keys fix for key logic (Basic reductions do not affect amount of keys placed while crossed do) --- DoorShuffle.py | 4 +++- DungeonGenerator.py | 1 + KeyDoorShuffle.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index ca4b9139..3ed21895 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1082,6 +1082,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Step 3: Initial valid combination find - reduce flex if needed for name, builder in dungeon_builders.items(): suggested = builder.key_doors_num - builder.key_drop_cnt + builder.total_keys = builder.key_doors_num find_valid_combination(builder, start_regions_map[name], world, player) actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt if actual_chest_keys < suggested: @@ -1098,6 +1099,7 @@ def assign_cross_keys(dungeon_builders, world, player): name = builder.name logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name) builder.key_doors_num += 1 + builder.total_keys = builder.key_doors_num result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False) if result: remaining -= 1 @@ -1391,7 +1393,7 @@ def shuffle_key_doors(builder, world, player): skips.append(world.get_door(dp.door_a, player)) break num_key_doors += 1 - builder.key_doors_num = num_key_doors + builder.key_doors_num = builder.total_keys = num_key_doors find_small_key_door_candidates(builder, start_regions, world, player) find_valid_combination(builder, start_regions, world, player) reassign_key_doors(builder, world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 3c64df89..98908868 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1190,6 +1190,7 @@ class DungeonBuilder(object): self.pre_open_stonewalls = set() # used by stonewall system self.candidates = None + self.total_keys = None self.key_doors_num = None self.combo_size = None self.flex = 0 diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7829d92b..84ff1b38 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1519,7 +1519,7 @@ def create_key_counters(key_layout, world, player): state.key_locations = default_key_counts[key_layout.sector.name] else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.key_doors_num - builder.key_drop_cnt + state.key_locations = builder.total_keys - builder.key_drop_cnt state.big_key_special = False for region in key_layout.sector.regions: for location in region.locations: From e3695e6d569e3fd8c240c3f7e103cd61aceec493 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 13 Aug 2021 03:37:01 -0500 Subject: [PATCH 041/104] Fixed issue with shuffling dropdowns in Mixed OW shuffle --- EntranceShuffle.py | 88 +++++++++++----------------------------------- 1 file changed, 21 insertions(+), 67 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 2d06b423..aea9ef58 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -50,18 +50,18 @@ def link_entrances(world, player): # inverted entrance mods for owid in swapped_connections.keys(): - if (world.mode[player] == 'inverted') != (owid in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag != (owid in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): for (entrancename, exitname) in swapped_connections[owid]: try: connect_two_way(world, entrancename, exitname, player) except RuntimeError: connect_entrance(world, entrancename, exitname, player) - if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ - (world.mode[player] == 'inverted') == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag != (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ + invFlag == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): connect_entrance(world, 'Death Mountain Return Cave (West)', 'Dark Death Mountain Healer Fairy', player) - elif (world.mode[player] == 'inverted') != (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ - (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + elif invFlag != (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ + invFlag == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): connect_two_way(world, 'Bumper Cave (Top)', 'Death Mountain Return Cave Exit (West)', player) # dungeon entrance shuffle @@ -239,10 +239,7 @@ def link_entrances(world, player): connect_caves(world, remaining_entrances if not invFlag else lw_dm_entrances, [], caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) # place blacksmith, has limited options if invFlag: @@ -371,10 +368,7 @@ def link_entrances(world, player): connect_caves(world, lw_entrances, dw_entrances, caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) doors = lw_entrances + dw_entrances @@ -632,10 +626,7 @@ def link_entrances(world, player): connect_caves(world, lw_entrances, dw_entrances, caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) doors = lw_entrances + dw_entrances @@ -776,10 +767,7 @@ def link_entrances(world, player): connect_caves(world, entrances, [], caves, player) # scramble holes - if not invFlag: - scramble_holes(world, player) - else: - scramble_inverted_holes(world, player) + scramble_holes(world, player) # place remaining doors connect_doors(world, entrances, door_targets, player) @@ -1601,8 +1589,12 @@ def scramble_holes(world, player): ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] if not world.shuffle_ganon: - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) else: hole_targets.append(('Pyramid Exit', 'Pyramid')) @@ -1620,50 +1612,12 @@ def scramble_holes(world, player): if world.shuffle_ganon: random.shuffle(hole_targets) exit, target = hole_targets.pop() - connect_two_way(world, 'Pyramid Entrance', exit, player) - connect_entrance(world, 'Pyramid Hole', target, player) - if world.shuffle[player] != 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - - random.shuffle(hole_targets) - for entrance, drop in hole_entrances: - exit, target = hole_targets.pop() - connect_two_way(world, entrance, exit, player) - connect_entrance(world, drop, target, player) - - -def scramble_inverted_holes(world, player): - hole_entrances = [('Kakariko Well Cave', 'Kakariko Well Drop'), - ('Bat Cave Cave', 'Bat Cave Drop'), - ('North Fairy Cave', 'North Fairy Cave Drop'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout Drop'), - ('Lumberjack Tree Cave', 'Lumberjack Tree Tree'), - ('Sanctuary', 'Sanctuary Grave')] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - - if not world.shuffle_ganon: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: - hole_targets.append(('Pyramid Exit', 'Pyramid')) - - - hole_entrances.append(('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop')) - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - - # do not shuffle sanctuary into pyramid hole unless shuffle is crossed - if world.shuffle[player] == 'crossed': - hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - random.shuffle(hole_targets) - exit, target = hole_targets.pop() - connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) - connect_entrance(world, 'Inverted Pyramid Hole', target, player) + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) if world.shuffle[player] != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) From 899e0444473681cbda4993b1f575a9af21877bba Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 13 Aug 2021 05:10:47 -0500 Subject: [PATCH 042/104] Separated default connections to grouped lists --- EntranceShuffle.py | 247 +++++++++++++++++++++++---------------------- 1 file changed, 126 insertions(+), 121 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index aea9ef58..abdc651d 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -29,7 +29,7 @@ def link_entrances(world, player): # if we do not shuffle, set default connections if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - for exitname, regionname in default_connections: + for exitname, regionname in default_connections + default_connector_connections + default_drop_connections + default_item_connections + default_shop_connections: connect_simple(world, exitname, regionname, player) if world.shuffle[player] == 'vanilla': for exitname, regionname in default_dungeon_connections: @@ -2662,7 +2662,7 @@ mandatory_connections = [('Links House S&Q', 'Links House'), ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), ('Ganon Drop', 'Bottom of Pyramid') - ] + ] open_mandatory_connections = [('Sanctuary S&Q', 'Sanctuary'), ('Other World S&Q', 'Pyramid Area')] @@ -2672,40 +2672,13 @@ inverted_mandatory_connections = [('Sanctuary S&Q', 'Dark Sanctuary Hint'), ('Dark Sanctuary Hint Exit', 'Dark Chapel Area')] # non-shuffled entrance links -default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), - ("Blinds Hideout", "Blinds Hideout"), - ('Dam', 'Dam'), - ('Lumberjack House', 'Lumberjack House'), - ('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance'), - ('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance'), - ('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Courtyard Northeast'), +default_connections = [('Lumberjack House', 'Lumberjack House'), ('Bonk Fairy (Light)', 'Bonk Fairy (Light)'), ('Lake Hylia Fairy', 'Lake Hylia Healer Fairy'), ('Lake Hylia Fortune Teller', 'Lake Hylia Fortune Teller'), ('Light Hype Fairy', 'Swamp Healer Fairy'), ('Desert Fairy', 'Desert Healer Fairy'), - ('Kings Grave', 'Kings Grave'), ('Tavern North', 'Tavern'), - ('Chicken House', 'Chicken House'), - ('Aginahs Cave', 'Aginahs Cave'), - ('Sahasrahlas Hut', 'Sahasrahlas Hut'), - ('Cave Shop (Lake Hylia)', 'Cave Shop (Lake Hylia)'), - ('Capacity Upgrade', 'Capacity Upgrade'), - ('Kakariko Well Drop', 'Kakariko Well (top)'), - ('Kakariko Well Cave', 'Kakariko Well (bottom)'), - ('Kakariko Well Exit', 'Kakariko Area'), - ('Blacksmiths Hut', 'Blacksmiths Hut'), - ('Bat Cave Drop', 'Bat Cave (right)'), - ('Bat Cave Cave', 'Bat Cave (left)'), - ('Bat Cave Exit', 'Blacksmith Area'), - ('Sick Kids House', 'Sick Kids House'), - ('Elder House (East)', 'Elder House'), - ('Elder House (West)', 'Elder House'), - ('Elder House Exit (East)', 'Kakariko Area'), - ('Elder House Exit (West)', 'Kakariko Area'), - ('North Fairy Cave Drop', 'North Fairy Cave'), - ('North Fairy Cave', 'North Fairy Cave'), - ('North Fairy Cave Exit', 'River Bend Area'), ('Lost Woods Gamble', 'Lost Woods Gamble'), ('Fortune Teller (Light)', 'Fortune Teller (Light)'), ('Snitch Lady (East)', 'Snitch Lady (East)'), @@ -2713,117 +2686,149 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Bush Covered House', 'Bush Covered House'), ('Tavern (Front)', 'Tavern (Front)'), ('Light World Bomb Hut', 'Light World Bomb Hut'), - ('Kakariko Shop', 'Kakariko Shop'), - ('Lost Woods Hideout Drop', 'Lost Woods Hideout (top)'), - ('Lost Woods Hideout Stump', 'Lost Woods Hideout (bottom)'), - ('Lost Woods Hideout Exit', 'Lost Woods East Area'), - ('Lumberjack Tree Tree', 'Lumberjack Tree (top)'), - ('Lumberjack Tree Cave', 'Lumberjack Tree (bottom)'), - ('Lumberjack Tree Exit', 'Lumberjack Area'), - ('Cave 45', 'Cave 45'), - ('Graveyard Cave', 'Graveyard Cave'), - ('Checkerboard Cave', 'Checkerboard Cave'), - ('Mini Moldorm Cave', 'Mini Moldorm Cave'), ('Long Fairy Cave', 'Long Fairy Cave'), # near East Light World Teleporter ('Good Bee Cave', 'Good Bee Cave'), ('20 Rupee Cave', '20 Rupee Cave'), ('50 Rupee Cave', '50 Rupee Cave'), - ('Ice Rod Cave', 'Ice Rod Cave'), - ('Bonk Rock Cave', 'Bonk Rock Cave'), - ('Library', 'Library'), ('Kakariko Gamble Game', 'Kakariko Gamble Game'), - ('Potion Shop', 'Potion Shop'), - ('Two Brothers House (East)', 'Two Brothers House'), - ('Two Brothers House (West)', 'Two Brothers House'), - ('Two Brothers House Exit (East)', 'Kakariko Suburb Area'), - ('Two Brothers House Exit (West)', 'Maze Race Ledge'), - - ('Sanctuary', 'Sanctuary Portal'), - ('Sanctuary Grave', 'Sewer Drop'), - ('Sanctuary Exit', 'Sanctuary Area'), - - ('Old Man Cave (West)', 'Old Man Cave Ledge'), - ('Old Man Cave (East)', 'Old Man Cave'), - ('Old Man Cave Exit (West)', 'Mountain Entry Entrance'), - ('Old Man Cave Exit (East)', 'West Death Mountain (Bottom)'), - ('Old Man House (Bottom)', 'Old Man House'), - ('Old Man House Exit (Bottom)', 'West Death Mountain (Bottom)'), - ('Old Man House (Top)', 'Old Man House Back'), - ('Old Man House Exit (Top)', 'West Death Mountain (Bottom)'), - ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'), - ('Death Mountain Return Cave (West)', 'Death Mountain Return Cave'), - ('Death Mountain Return Cave Exit (West)', 'Mountain Entry Ledge'), - ('Death Mountain Return Cave Exit (East)', 'West Death Mountain (Bottom)'), - ('Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Peak)'), - ('Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave (Bottom)'), - ('Spectacle Rock Cave', 'Spectacle Rock Cave (Top)'), - ('Spectacle Rock Cave Exit', 'West Death Mountain (Bottom)'), - ('Spectacle Rock Cave Exit (Top)', 'West Death Mountain (Bottom)'), - ('Spectacle Rock Cave Exit (Peak)', 'West Death Mountain (Bottom)'), - ('Paradox Cave (Bottom)', 'Paradox Cave Front'), - ('Paradox Cave (Middle)', 'Paradox Cave'), - ('Paradox Cave (Top)', 'Paradox Cave'), - ('Paradox Cave Exit (Bottom)', 'East Death Mountain (Bottom)'), - ('Paradox Cave Exit (Middle)', 'East Death Mountain (Bottom)'), - ('Paradox Cave Exit (Top)', 'East Death Mountain (Top East)'), ('Hookshot Fairy', 'Hookshot Fairy'), - ('Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Bottom)'), - ('Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Top)'), - ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Plateau'), - ('Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Ledge'), - ('Spiral Cave', 'Spiral Cave (Top)'), - ('Spiral Cave (Bottom)', 'Spiral Cave (Bottom)'), - ('Spiral Cave Exit', 'East Death Mountain (Bottom)'), - ('Spiral Cave Exit (Top)', 'Spiral Cave Ledge'), - - ('Pyramid Fairy', 'Pyramid Fairy'), + ('East Dark World Hint', 'East Dark World Hint'), ('Palace of Darkness Hint', 'Palace of Darkness Hint'), - ('Dark Lake Hylia Shop', 'Dark Lake Hylia Shop'), ('Dark Lake Hylia Fairy', 'Dark Lake Hylia Healer Fairy'), ('Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Healer Fairy'), ('Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Spike Cave'), ('Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Hint'), - ('Hype Cave', 'Hype Cave'), ('Bonk Fairy (Dark)', 'Bonk Fairy (Dark)'), - ('Brewery', 'Brewery'), - ('C-Shaped House', 'C-Shaped House'), - ('Chest Game', 'Chest Game'), - ('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'), - ('Bumper Cave (Bottom)', 'Bumper Cave'), - ('Bumper Cave (Top)', 'Bumper Cave'), - ('Red Shield Shop', 'Red Shield Shop'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), - ('Dark World Shop', 'Village of Outcasts Shop'), - ('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'), - ('Dark World Potion Shop', 'Dark World Potion Shop'), ('Archery Game', 'Archery Game'), - ('Bumper Cave Exit (Top)', 'Bumper Cave Ledge'), - ('Bumper Cave Exit (Bottom)', 'Bumper Cave Entrance'), - ('Mire Shed', 'Mire Shed'), ('Dark Desert Hint', 'Dark Desert Hint'), ('Dark Desert Fairy', 'Dark Desert Healer Fairy'), - ('Spike Cave', 'Spike Cave'), - ('Hookshot Cave', 'Hookshot Cave (Front)'), - ('Superbunny Cave (Top)', 'Superbunny Cave (Top)'), - ('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'), ('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'), - ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), - ('Superbunny Cave Exit (Top)', 'East Dark Death Mountain (Top)'), - ('Superbunny Cave Exit (Bottom)', 'East Dark Death Mountain (Bottom)'), - ('Hookshot Cave Front Exit', 'East Dark Death Mountain (Top)'), - ('Hookshot Cave Back Exit', 'Dark Death Mountain Floating Island'), - ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), - ('Mimic Cave', 'Mimic Cave'), - - ('Pyramid Exit', 'Pyramid Exit Ledge'), - ('Pyramid Hole', 'Pyramid'), - ('Pyramid Entrance', 'Bottom of Pyramid'), - ('Inverted Pyramid Hole', 'Pyramid'), - ('Inverted Pyramid Entrance', 'Bottom of Pyramid') ] +default_connector_connections = [('Old Man Cave (West)', 'Old Man Cave Ledge'), + ('Old Man Cave (East)', 'Old Man Cave'), + ('Old Man Cave Exit (West)', 'Mountain Entry Entrance'), + ('Old Man Cave Exit (East)', 'West Death Mountain (Bottom)'), + ('Old Man House (Bottom)', 'Old Man House'), + ('Old Man House Exit (Bottom)', 'West Death Mountain (Bottom)'), + ('Old Man House (Top)', 'Old Man House Back'), + ('Old Man House Exit (Top)', 'West Death Mountain (Bottom)'), + ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'), + ('Death Mountain Return Cave (West)', 'Death Mountain Return Cave'), + ('Death Mountain Return Cave Exit (West)', 'Mountain Entry Ledge'), + ('Death Mountain Return Cave Exit (East)', 'West Death Mountain (Bottom)'), + ('Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Peak)'), + ('Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave (Bottom)'), + ('Spectacle Rock Cave', 'Spectacle Rock Cave (Top)'), + ('Spectacle Rock Cave Exit', 'West Death Mountain (Bottom)'), + ('Spectacle Rock Cave Exit (Top)', 'West Death Mountain (Bottom)'), + ('Spectacle Rock Cave Exit (Peak)', 'West Death Mountain (Bottom)'), + ('Spiral Cave', 'Spiral Cave (Top)'), + ('Spiral Cave (Bottom)', 'Spiral Cave (Bottom)'), + ('Spiral Cave Exit', 'East Death Mountain (Bottom)'), + ('Spiral Cave Exit (Top)', 'Spiral Cave Ledge'), + ('Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Bottom)'), + ('Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Top)'), + ('Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Plateau'), + ('Fairy Ascension Cave Exit (Top)', 'Fairy Ascension Ledge'), + ('Paradox Cave (Bottom)', 'Paradox Cave Front'), + ('Paradox Cave (Middle)', 'Paradox Cave'), + ('Paradox Cave (Top)', 'Paradox Cave'), + ('Paradox Cave Exit (Bottom)', 'East Death Mountain (Bottom)'), + ('Paradox Cave Exit (Middle)', 'East Death Mountain (Bottom)'), + ('Paradox Cave Exit (Top)', 'East Death Mountain (Top East)'), + ('Elder House (East)', 'Elder House'), + ('Elder House (West)', 'Elder House'), + ('Elder House Exit (East)', 'Kakariko Area'), + ('Elder House Exit (West)', 'Kakariko Area'), + ('Two Brothers House (East)', 'Two Brothers House'), + ('Two Brothers House (West)', 'Two Brothers House'), + ('Two Brothers House Exit (East)', 'Kakariko Suburb Area'), + ('Two Brothers House Exit (West)', 'Maze Race Ledge'), + ('Bumper Cave (Bottom)', 'Bumper Cave'), + ('Bumper Cave (Top)', 'Bumper Cave'), + ('Bumper Cave Exit (Top)', 'Bumper Cave Ledge'), + ('Bumper Cave Exit (Bottom)', 'Bumper Cave Entrance'), + ('Superbunny Cave (Top)', 'Superbunny Cave (Top)'), + ('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'), + ('Superbunny Cave Exit (Top)', 'East Dark Death Mountain (Top)'), + ('Superbunny Cave Exit (Bottom)', 'East Dark Death Mountain (Bottom)'), + ('Hookshot Cave', 'Hookshot Cave (Front)'), + ('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'), + ('Hookshot Cave Front Exit', 'East Dark Death Mountain (Top)'), + ('Hookshot Cave Back Exit', 'Dark Death Mountain Floating Island') + ] + +default_item_connections = [('Mimic Cave', 'Mimic Cave'), + ('Waterfall of Wishing', 'Waterfall of Wishing'), + ('Bonk Rock Cave', 'Bonk Rock Cave'), + ('Graveyard Cave', 'Graveyard Cave'), + ('Kings Grave', 'Kings Grave'), + ('Potion Shop', 'Potion Shop'), + ('Blinds Hideout', 'Blinds Hideout'), + ('Chicken House', 'Chicken House'), + ('Sick Kids House', 'Sick Kids House'), + ('Sahasrahlas Hut', 'Sahasrahlas Hut'), + ('Blacksmiths Hut', 'Blacksmiths Hut'), + ('Library', 'Library'), + ('Checkerboard Cave', 'Checkerboard Cave'), + ('Aginahs Cave', 'Aginahs Cave'), + ('Cave 45', 'Cave 45'), + ('Mini Moldorm Cave', 'Mini Moldorm Cave'), + ('Ice Rod Cave', 'Ice Rod Cave'), + ('Dam', 'Dam'), + ('Spike Cave', 'Spike Cave'), + ('Chest Game', 'Chest Game'), + ('C-Shaped House', 'C-Shaped House'), + ('Brewery', 'Brewery'), + ('Pyramid Fairy', 'Pyramid Fairy'), + ('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'), + ('Mire Shed', 'Mire Shed'), + ('Hype Cave', 'Hype Cave') + ] + +default_shop_connections = [('Kakariko Shop', 'Kakariko Shop'), + ('Cave Shop (Lake Hylia)', 'Cave Shop (Lake Hylia)'), + ('Capacity Upgrade', 'Capacity Upgrade'), + ('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'), + ('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'), + ('Dark World Potion Shop', 'Dark World Potion Shop'), + ('Dark World Shop', 'Village of Outcasts Shop'), + ('Red Shield Shop', 'Red Shield Shop'), + ('Dark Lake Hylia Shop', 'Dark Lake Hylia Shop') + ] + +default_drop_connections = [('Lost Woods Hideout Drop', 'Lost Woods Hideout (top)'), + ('Lost Woods Hideout Stump', 'Lost Woods Hideout (bottom)'), + ('Lost Woods Hideout Exit', 'Lost Woods East Area'), + ('Lumberjack Tree Tree', 'Lumberjack Tree (top)'), + ('Lumberjack Tree Cave', 'Lumberjack Tree (bottom)'), + ('Lumberjack Tree Exit', 'Lumberjack Area'), + ('Sanctuary', 'Sanctuary Portal'), + ('Sanctuary Grave', 'Sewer Drop'), + ('Sanctuary Exit', 'Sanctuary Area'), + ('North Fairy Cave Drop', 'North Fairy Cave'), + ('North Fairy Cave', 'North Fairy Cave'), + ('North Fairy Cave Exit', 'River Bend Area'), + ('Kakariko Well Drop', 'Kakariko Well (top)'), + ('Kakariko Well Cave', 'Kakariko Well (bottom)'), + ('Kakariko Well Exit', 'Kakariko Area'), + ('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance'), + ('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance'), + ('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Courtyard Northeast'), + ('Bat Cave Drop', 'Bat Cave (right)'), + ('Bat Cave Cave', 'Bat Cave (left)'), + ('Bat Cave Exit', 'Blacksmith Area'), + ('Pyramid Hole', 'Pyramid'), + ('Pyramid Entrance', 'Bottom of Pyramid'), + ('Inverted Pyramid Hole', 'Pyramid'), + ('Inverted Pyramid Entrance', 'Bottom of Pyramid'), + ('Pyramid Exit', 'Pyramid Exit Ledge') + ] + swapped_connections = { 0x03: [ ('Old Man Cave (East)', 'Death Mountain Return Cave Exit (West)'), @@ -2844,7 +2849,7 @@ swapped_connections = { open_default_connections = [('Links House', 'Links House'), ('Links House Exit', 'Links House Area'), ('Big Bomb Shop', 'Big Bomb Shop') - ] + ] inverted_default_connections = [('Links House', 'Big Bomb Shop'), ('Links House Exit', 'Big Bomb Shop Area'), From 2b4c4a6595432ccf3487a0213b0bc784d9f231bd Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 13 Aug 2021 05:31:07 -0500 Subject: [PATCH 043/104] Various Mixed OW Shuffle (Inverted) fixes --- EntranceShuffle.py | 57 ++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index abdc651d..9b31b652 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -225,7 +225,7 @@ def link_entrances(world, player): random.shuffle(remaining_entrances) old_man_entrance = remaining_entrances.pop() - connect_two_way(world, old_man_entrance if not invFlag else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + connect_two_way(world, old_man_entrance if invFlag == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) if invFlag and old_man_exit == 'Spike Cave': bomb_shop_doors.remove('Spike Cave') @@ -469,9 +469,7 @@ def link_entrances(world, player): connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) elif invFlag or world.doorShuffle[player] == 'vanilla': caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - lw_entrances.append('Hyrule Castle Entrance (South)') - else: - lw_entrances.append('Hyrule Castle Entrance (South)') + lw_entrances.append('Hyrule Castle Entrance (South)') if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) @@ -1166,20 +1164,28 @@ def link_entrances(world, player): if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance' if not invFlag else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole' if not invFlag else 'Inverted Pyramid Hole', 'Pyramid', player) + connect_two_way(world, 'Pyramid Entrance' if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Inverted Pyramid Hole', 'Pyramid', player) else: caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) hole_targets.append('Pyramid') + if not invFlag: - exit_pool.extend(['Ganons Tower', 'Pyramid Entrance']) + exit_pool.extend(['Ganons Tower']) + doors.extend(['Ganons Tower']) + else: + exit_pool.extend(['Agahnims Tower']) + doors.extend(['Agahnims Tower']) + + if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + exit_pool.extend(['Pyramid Entrance']) hole_entrances.append('Pyramid Hole') entrances_must_exits.append('Pyramid Entrance') - doors.extend(['Ganons Tower', 'Pyramid Entrance']) + doors.extend(['Pyramid Entrance']) else: - exit_pool.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) + exit_pool.extend(['Inverted Pyramid Entrance']) hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) + doors.extend(['Inverted Pyramid Entrance']) random.shuffle(hole_entrances) random.shuffle(hole_targets) @@ -1355,24 +1361,31 @@ def link_entrances(world, player): if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - if not invFlag: + if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) else: connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) else: - caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) hole_targets.append('Pyramid') + if not invFlag: + doors.extend(['Ganons Tower']) + exit_pool.extend(['Ganons Tower']) + else: + doors.extend(['Agahnims Tower']) + exit_pool.extend(['Agahnims Tower']) + + if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): hole_entrances.append('Pyramid Hole') - doors.extend(['Agahnims Tower', 'Pyramid Entrance']) - exit_pool.extend(['Agahnims Tower', 'Pyramid Entrance']) + doors.extend(['Pyramid Entrance']) + exit_pool.extend(['Pyramid Entrance']) else: hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) - exit_pool.extend(['Agahnims Tower', 'Inverted Pyramid Entrance']) + doors.extend(['Inverted Pyramid Entrance']) + exit_pool.extend(['Inverted Pyramid Entrance']) random.shuffle(hole_entrances) random.shuffle(hole_targets) @@ -1502,7 +1515,7 @@ def link_entrances(world, player): world.powder_patch_required[player] = True # check for ganon location - if world.get_entrance('Pyramid Hole' if not invFlag else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': + if world.get_entrance('Pyramid Hole' if invFlag == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': world.ganon_at_pyramid[player] = False # check for Ganon's Tower location @@ -2926,12 +2939,12 @@ inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Portal' indirect_connections = { 'Turtle Rock Ledge': 'Turtle Rock', 'Pyramid Area': 'Pyramid Fairy', - 'East Dark World': 'Pyramid Fairy', + #'East Dark World': 'Pyramid Fairy', 'Big Bomb Shop': 'Pyramid Fairy', - 'Dark Desert': 'Pyramid Fairy', - 'West Dark World': 'Pyramid Fairy', - 'South Dark World': 'Pyramid Fairy', - 'Light World': 'Pyramid Fairy', + #'Dark Desert': 'Pyramid Fairy', + #'West Dark World': 'Pyramid Fairy', + #'South Dark World': 'Pyramid Fairy', + #'Light World': 'Pyramid Fairy', 'Old Man Cave': 'Old Man S&Q' } # format: From 651f057adc3b5a03ae2518d7734dd1492e5da800 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 13 Aug 2021 05:36:49 -0500 Subject: [PATCH 044/104] Version bump 0.1.7.4 --- CHANGELOG.md | 5 +++++ OverworldShuffle.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f08a41..e97665ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 0.1.7.4 +- Fixed issue with Mixed OW failing to generate when HC/Pyramid is swapped +- Various fixes to improve generation rates for Mixed OW Shuffle +- ~~Merged DR v0.5.0.2 - Shuffle SFX~~ + ### 0.1.7.3 - Fixed minor issue with ambient SFX stopping and starting on OW screen load - MSU-1 changed to play LW2 (track 60) when Aga1 is killed instead of ped pull diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6aa22ece..881cffdd 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.7.3-u' +__version__ = '0.1.7.4-u' def link_overworld(world, player): # setup mandatory connections From 678e5eb091ae02459a8be279e92502f7161044e0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 13 Aug 2021 14:41:04 -0600 Subject: [PATCH 045/104] Multi-entrance crystal doesn't propagate between dungeons Multi-entrance: don't use unreachable entrances for reachable stats --- DungeonGenerator.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 98908868..170233ce 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -102,8 +102,15 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon excluded = {} for region in entrance_regions: portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) - if portal and portal.destination: - excluded[region] = None + if portal: + if portal.destination: + excluded[region] = None + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if access_region.name in world.inaccessible_regions[player]: + excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} all_regions = set() @@ -855,6 +862,8 @@ class ExplorationState(object): return exp_door def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False): + if region.type != RegionType.Dungeon: + self.crystal = CrystalBarrier.Orange if self.crystal == CrystalBarrier.Either: if region not in self.visited_blue: self.visited_blue.append(region) From 54004118f5f874881b4a1820741f35f20e0a6b49 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 13 Aug 2021 14:41:35 -0600 Subject: [PATCH 046/104] Minor bug during exploration --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index d6e0c5e2..91384e46 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -518,7 +518,7 @@ class CollectionState(object): connection, crystal_state = queue.popleft() new_region = connection.connected_region if not self.should_visit(new_region, rrp, crystal_state, player): - if not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): + if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player): bc.pop(connection, None) elif connection.can_reach(self): bc.pop(connection, None) From cda70f783a375e642d61ac4fc08690fc620af59b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 13 Aug 2021 22:11:58 -0500 Subject: [PATCH 047/104] Removal of legacy ER shuffles --- BaseClasses.py | 10 +- ER_hint_reference.txt | 2 +- EntranceShuffle.py | 572 ------------------ ItemList.py | 6 +- resources/app/cli/args.json | 4 - resources/app/cli/lang/en.json | 4 +- .../app/gui/randomize/entrando/widgets.json | 4 - 7 files changed, 10 insertions(+), 592 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7e866f5d..32028111 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2553,11 +2553,11 @@ class Pot(object): self.flags = flags -# byte 0: DDOO EEEE (DR, OR, ER) +# byte 0: DDOO OEEE (DR, OR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} -or_mode = {"parallel": 1, "full": 2, "vanilla": 0} -er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, "restricted_legacy": 8, - "full_legacy": 9, "madness_legacy": 10, "insanity_legacy": 11, "dungeonsfull": 7, "dungeonssimple": 6} +or_mode = {"vanilla": 0, "parallel": 1, "full": 2} +orswap_mode = {"vanilla": 0, "mixed": 1, "crossed": 1} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 3, "full": 3, "crossed": 4, "insanity": 5, "dungeonsfull": 7, "dungeonssimple": 7} # byte 1: LLLW WSSR (logic, mode, sword, retro) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4} @@ -2592,7 +2592,7 @@ class Settings(object): @staticmethod def make_code(w, p): code = bytes([ - (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 4) | er_mode[w.shuffle[p]], + (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 4) | (orswap_mode[w.owSwap[p]] << 3) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) | (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0), diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index 999fb436..8bc73fc9 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -14,7 +14,7 @@ In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios 8 hints for valuable items. 7 junk hints. -In the simple, restricted, and restricted legacy shuffles, these are the ratios: +In the simple, restricted shuffles, these are the ratios: 2 hints for inconvenient entrances. 1 hint for an inconvenient dungeon entrance. diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 9b31b652..dd98a9ed 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -374,61 +374,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif not invFlag and world.shuffle[player] == 'restricted_legacy': - simple_shuffle_dungeons(world, player) - - lw_entrances = list(LW_Entrances) - dw_entrances = list(DW_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances) - caves = list(Cave_Exits) - three_exit_caves = list(Cave_Three_Exits) - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # only use two exit caves to do mandatory dw connections - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - # add three exit doors to pool for remainder - caves.extend(three_exit_caves) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.extend(old_man_entrances) - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], Old_Man_House, player) - - # connect rest. There's 2 dw entrances remaining, so we will not run into parity issue placing caves - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) elif world.shuffle[player] == 'full': skull_woods_shuffle(world, player) @@ -769,324 +714,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, entrances, door_targets, player) - elif not invFlag and world.shuffle[player] == 'full_legacy': - skull_woods_shuffle(world, player) - - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera']) - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - else: - caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3))) - lw_entrances.append('Hyrule Castle Entrance (South)') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if random.randint(0, 1) == 0: - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - else: - connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player) - connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player) - if world.mode[player] == 'standard': - # rest of hyrule castle must be in light world - connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [door for door in old_man_entrances if door in lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - random.shuffle(lw_entrances) - old_man_entrance = lw_entrances.pop() - connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player) - connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) - - # place Old Man House in Light World - connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #need this to avoid badness with multiple seeds - - # now scramble the rest - connect_caves(world, lw_entrances, dw_entrances, caves, player) - - # scramble holes - scramble_holes(world, player) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place bomb shop, has limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) - elif not invFlag and world.shuffle[player] == 'madness_legacy': - # here lie dragons, connections are no longer two way - lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances) - dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances) - dw_entrances_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) - - lw_doors = list(LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit) + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', - 'Lumberjack Tree Cave'] + list(Old_Man_Entrances) - dw_doors = list(DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - random.shuffle(lw_doors) - random.shuffle(dw_doors) - - dw_entrances_must_exits.append('Skull Woods Second Section Door (West)') - dw_entrances.append('Skull Woods Second Section Door (East)') - dw_entrances.append('Skull Woods First Section Door') - - lw_entrances.extend(['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']) - - lw_entrances_must_exits = list(LW_Dungeon_Entrances_Must_Exit) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - mandatory_light_world = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)'] - mandatory_dark_world = [] - caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) - - # shuffle up holes - - lw_hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave'] - dw_hole_entrances = ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'), - ('Bat Cave Exit', 'Bat Cave (right)'), - ('North Fairy Cave Exit', 'North Fairy Cave'), - ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), - ('Lumberjack Tree Exit', 'Lumberjack Tree (top)'), - (('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Back Drop')] - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - lw_hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance')) - lw_doors.append('Hyrule Castle Secret Entrance Stairs') - lw_entrances.append('Hyrule Castle Secret Entrance Stairs') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - dw_entrances.append('Ganons Tower') - caves.append('Ganons Tower Exit') - dw_hole_entrances.append('Pyramid Hole') - hole_targets.append(('Pyramid Exit', 'Pyramid')) - dw_entrances_must_exits.append('Pyramid Entrance') - dw_doors.extend(['Ganons Tower', 'Pyramid Entrance']) - - random.shuffle(lw_hole_entrances) - random.shuffle(dw_hole_entrances) - random.shuffle(hole_targets) - - # decide if skull woods first section should be in light or dark world - sw_light = random.randint(0, 1) == 0 - if sw_light: - sw_hole_pool = lw_hole_entrances - mandatory_light_world.append('Skull Woods First Section Exit') - else: - sw_hole_pool = dw_hole_entrances - mandatory_dark_world.append('Skull Woods First Section Exit') - for target in ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle']: - connect_entrance(world, sw_hole_pool.pop(), target, player) - - # sanctuary has to be in light world - connect_entrance(world, lw_hole_entrances.pop(), 'Sewer Drop', player) - mandatory_light_world.append('Sanctuary Exit') - - # fill up remaining holes - for hole in dw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_dark_world.append(exits) - connect_entrance(world, hole, target, player) - - for hole in lw_hole_entrances: - exits, target = hole_targets.pop() - mandatory_light_world.append(exits) - connect_entrance(world, hole, target, player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - mandatory_light_world.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - lw_doors.append('Hyrule Castle Entrance (South)') - lw_entrances.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock and Spectracle Rock cave have two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, general, worldspecific, worldoors): - # select which one is the primary option - if random.randint(0, 1) == 0: - primary = general - secondary = worldspecific - else: - primary = worldspecific - secondary = general - - try: - cave = extract_reachable_exit(primary) - except RuntimeError: - cave = extract_reachable_exit(secondary) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, worldoors.pop(), exit, player) - # rest of cave now is forced to be in this world - worldspecific.append(cave) - - # we randomize which world requirements we fulfill first so we get better dungeon distribution - if random.randint(0, 1) == 0: - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - else: - for entrance in dw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors) - for entrance in lw_entrances_must_exits: - connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in lw_entrances] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - lw_entrances.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, lw_doors.pop(), 'Old Man Cave Exit (East)', player) - mandatory_light_world.append('Old Man Cave Exit (West)') - - # we connect up the mandatory associations we have found - for mandatory in mandatory_light_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, lw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, lw_doors.pop(), exit, player) - - for mandatory in mandatory_dark_world: - if not isinstance(mandatory, tuple): - mandatory = (mandatory,) - for exit in mandatory: - # point out somewhere - connect_exit(world, exit, dw_entrances.pop(), player) - # point in from somewhere - connect_entrance(world, dw_doors.pop(), exit, player) - - # handle remaining caves - while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill - cave_candidate = (None, 0) - for i, cave in enumerate(caves): - if isinstance(cave, str): - cave = (cave,) - if len(cave) > cave_candidate[1]: - cave_candidate = (i, len(cave)) - cave = caves.pop(cave_candidate[0]) - - place_lightworld = random.randint(0, 1) == 0 - if place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - if isinstance(cave, str): - cave = (cave,) - - # check if we can still fit the cave into our target group - if len(target_doors) < len(cave): - if not place_lightworld: - target_doors = lw_doors - target_entrances = lw_entrances - else: - target_doors = dw_doors - target_entrances = dw_entrances - - for exit in cave: - connect_exit(world, exit, target_entrances.pop(), player) - connect_entrance(world, target_doors.pop(), exit, player) - - # handle simple doors - - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - bomb_shop_doors.extend(blacksmith_doors) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - single_doors.extend(bomb_shop_doors) - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) elif world.shuffle[player] == 'insanity': # beware ye who enter here @@ -1304,205 +931,6 @@ def link_entrances(world, player): # place remaining doors connect_doors(world, doors, door_targets, player) - elif world.shuffle[player] == 'insanity_legacy': - world.fix_fake_world[player] = False - # beware ye who enter here - - if not invFlag: - entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)'] - - doors = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] - else: - entrances_must_exits = Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit - - doors = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Entrances_Must_Exit + Inverted_LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Secret Entrance Stairs'] + Inverted_Old_Man_Entrances +\ - Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] +\ - Inverted_LW_Single_Cave_Doors + Inverted_DW_Single_Cave_Doors + ['Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'] - - exit_pool = list(doors) - - # randomize which desert ledge door is a must-exit - if random.randint(0, 1) == 0: - entrances_must_exits.append('Desert Palace Entrance (North)') - else: - entrances_must_exits.append('Desert Palace Entrance (West)') - - doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\ - DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] - - random.shuffle(doors) - - old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera'] - - caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', - 'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit'] - - # shuffle up holes - - hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', - 'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'] - - hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop', - 'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle'] - - # tavern back door cannot be shuffled yet - connect_doors(world, ['Tavern North'], ['Tavern'], player) - - if world.mode[player] == 'standard': - # cannot move uncle cave - connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player) - connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player) - connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) - else: - hole_entrances.append('Hyrule Castle Secret Entrance Drop') - hole_targets.append('Hyrule Castle Secret Entrance') - doors.append('Hyrule Castle Secret Entrance Stairs') - caves.append('Hyrule Castle Secret Entrance Exit') - - if not world.shuffle_ganon: - connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player) - if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: - caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) - hole_targets.append('Pyramid') - - if not invFlag: - doors.extend(['Ganons Tower']) - exit_pool.extend(['Ganons Tower']) - else: - doors.extend(['Agahnims Tower']) - exit_pool.extend(['Agahnims Tower']) - - if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): - hole_entrances.append('Pyramid Hole') - doors.extend(['Pyramid Entrance']) - exit_pool.extend(['Pyramid Entrance']) - else: - hole_entrances.append('Inverted Pyramid Hole') - doors.extend(['Inverted Pyramid Entrance']) - exit_pool.extend(['Inverted Pyramid Entrance']) - - random.shuffle(hole_entrances) - random.shuffle(hole_targets) - random.shuffle(exit_pool) - - # fill up holes - for hole in hole_entrances: - connect_entrance(world, hole, hole_targets.pop(), player) - - # hyrule castle handling - if world.mode[player] == 'standard': - # must connect front of hyrule castle to do escape - connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player) - connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player) - caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - else: - doors.append('Hyrule Castle Entrance (South)') - caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')) - - if not world.shufflelinks[player]: - if not invFlag: - links_house = 'Links House' - else: - links_house = 'Big Shop Shop' - else: - if not invFlag: - links_house_doors = [i for i in doors if i not in Sanctuary_Doors + Isolated_LH_Doors] - else: - links_house_doors = [i for i in doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors] - links_house = random.choice(links_house_doors) - connect_two_way(world, links_house, 'Links House Exit', player) - connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should always match link's house, except for plandos - doors.remove(links_house) - exit_pool.remove(links_house) - - if not invFlag: - sanc_doors = [door for door in exit_pool] #[door for door in Sanctuary_Doors if door in exit_pool] - else: - sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in exit_pool] - sanc_door = random.choice(sanc_doors) - exit_pool.remove(sanc_door) - doors.remove(sanc_door) - connect_entrance(world, sanc_door, 'Dark Sanctuary Hint', player) - world.get_entrance('Dark Sanctuary Hint Exit', player).connect(world.get_entrance(sanc_door, player).parent_region) - - # now let's deal with mandatory reachable stuff - def extract_reachable_exit(cavelist): - random.shuffle(cavelist) - candidate = None - for cave in cavelist: - if isinstance(cave, tuple) and len(cave) > 1: - # special handling: TRock has two entries that we should consider entrance only - # ToDo this should be handled in a more sensible manner - if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2: - continue - candidate = cave - break - if candidate is None: - raise RuntimeError('No suitable cave.') - cavelist.remove(candidate) - return candidate - - def connect_reachable_exit(entrance, caves, doors, exit_pool): - cave = extract_reachable_exit(caves) - - exit = cave[-1] - cave = cave[:-1] - connect_exit(world, exit, entrance, player) - connect_entrance(world, doors.pop(), exit, player) - # rest of cave now is forced to be in this world - exit_pool.remove(entrance) - caves.append(cave) - - # connect mandatory exits - for entrance in entrances_must_exits: - connect_reachable_exit(entrance, caves, doors, exit_pool) - - # place old man, has limited options - # exit has to come from specific set of doors, the entrance is free to move about - old_man_entrances = [entrance for entrance in old_man_entrances if entrance in exit_pool] - random.shuffle(old_man_entrances) - old_man_exit = old_man_entrances.pop() - exit_pool.remove(old_man_exit) - - connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player) - connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player) - caves.append('Old Man Cave Exit (West)') - - # handle simple doors - single_doors = list(Single_Cave_Doors) - bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors) - blacksmith_doors = list(Blacksmith_Single_Cave_Doors) - door_targets = list(Single_Cave_Targets) - - # place blacksmith, has limited options - random.shuffle(blacksmith_doors) - blacksmith_hut = blacksmith_doors.pop() - connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player) - doors.remove(blacksmith_hut) - - # place dam and pyramid fairy, have limited options - random.shuffle(bomb_shop_doors) - bomb_shop = bomb_shop_doors.pop() - connect_entrance(world, bomb_shop, 'Big Bomb Shop', player) - doors.remove(bomb_shop) - - # handle remaining caves - for cave in caves: - if isinstance(cave, str): - cave = (cave,) - - for exit in cave: - connect_exit(world, exit, exit_pool.pop(), player) - connect_entrance(world, doors.pop(), exit, player) - - # place remaining doors - connect_doors(world, single_doors, door_targets, player) else: raise NotImplementedError('Shuffling not supported yet') diff --git a/ItemList.py b/ItemList.py index 44b8edc0..3d5c3a15 100644 --- a/ItemList.py +++ b/ItemList.py @@ -986,12 +986,12 @@ def test(): for mode in ['open', 'standard', 'inverted', 'retro']: for swords in ['random', 'assured', 'swordless', 'vanilla']: for progressive in ['on', 'off']: - for shuffle in ['full', 'insanity_legacy']: + for shuffle in ['vanilla', 'full', 'crossed', 'insanity']: for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: for door_shuffle in ['basic', 'crossed', 'vanilla']: - for owShuffle in ['full', 'vanilla']: - for owSwap in ['mixed', 'vanilla']: + for owShuffle in ['full', 'parallel', 'vanilla']: + for owSwap in ['vanilla', 'mixed', 'crossed']: out = get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) count = len(out[0]) + len(out[1]) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 7724ae9e..fe83b648 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -142,10 +142,6 @@ "full", "crossed", "insanity", - "restricted_legacy", - "full_legacy", - "madness_legacy", - "insanity_legacy", "dungeonsfull", "dungeonssimple" ] diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index b1691e53..9a2a647c 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -189,10 +189,8 @@ " which they are entered.", "Vanilla: All entrances are in the same locations they were", " in the base game.", - "Legacy shuffles preserve behavior from older versions of the", - "entrance randomizer including significant technical limitations.", "The dungeon variants only mix up dungeons and keep the rest of", - "the overworld vanilla." + "the entrances vanilla." ], "ow_shuffle": [ "This shuffles the layout of the overworld.", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index 7701936b..ffeeb976 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -12,10 +12,6 @@ "full", "crossed", "insanity", - "restricted_legacy", - "full_legacy", - "madness_legacy", - "insanity_legacy", "dungeonsfull", "dungeonssimple" ] From 181e6e6f846e788ab877ee9e6b1aaa0cdb73c5f4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 16 Aug 2021 00:38:52 -0500 Subject: [PATCH 048/104] Minor fix Pyramid Entrance hint in Insanity ER --- Rom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index bc4a9f02..217582c1 100644 --- a/Rom.py +++ b/Rom.py @@ -2177,10 +2177,10 @@ def write_strings(rom, world, player, team): if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: - if world.mode[player] == 'inverted': + if world.mode[player] == 'inverted' != (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) else: - entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) + entrances_to_hint.update({'Pyramid Entrance': 'The pyramid ledge'}) hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0 for entrance in all_entrances: if entrance.name in entrances_to_hint: From 7b81c5d9da3284c2b20aa673edc7e9cb2edccab2 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 16 Aug 2021 00:55:07 -0500 Subject: [PATCH 049/104] Minor code cleanup --- OverworldShuffle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 881cffdd..d1a3784f 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -152,8 +152,7 @@ def link_overworld(world, player): # layout shuffle if world.owShuffle[player] == 'vanilla': - for grouping in (trimmed_groups,): - groups = list(trimmed_groups.values()) + groups = list(trimmed_groups.values()) for (forward_edge_sets, back_edge_sets) in groups: assert len(forward_edge_sets) == len(back_edge_sets) for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): From b53a005545d456c719e809a5bd37a8adacab1c09 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 16 Aug 2021 15:28:20 -0600 Subject: [PATCH 050/104] Fix for last ditch problems Special bk adjustments Exception for self locking key doors in key lock checker --- BaseClasses.py | 5 ++++- Fill.py | 8 ++++++-- KeyDoorShuffle.py | 26 +++++++++++++++++++++++++- Main.py | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 91384e46..84ff319d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -432,7 +432,7 @@ class World(object): else: return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1))) - def can_beat_game(self, starting_state=None): + def can_beat_game(self, starting_state=None, log_error=False): if starting_state: if self.has_beaten_game(starting_state): return True @@ -456,6 +456,9 @@ class World(object): if not sphere: # ran out of places and did not finish yet, quit + if log_error: + missing_locations = ", ".join([x.name for x in prog_locations]) + logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}') return False for location in sphere: diff --git a/Fill.py b/Fill.py index 9adb0916..c42b6251 100644 --- a/Fill.py +++ b/Fill.py @@ -295,12 +295,14 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite if swap_spot: logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') world.push_item(swap_spot, old_item, False) + swap_spot.event = True locations.remove(swap_spot) locations.append(new_spot) return new_spot else: new_spot.item = restore_item - + else: + location.item = old_item return None @@ -315,10 +317,12 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, for location in locations: maximum_exploration_state = sweep_from_pool() perform_access_check = True + old_item = None if world.accessibility[item_to_place.player] == 'none': perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state) if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + old_item = location.item location.item = item_to_place test_state = maximum_exploration_state.copy() test_state.stale[item_to_place.player] = True @@ -329,7 +333,7 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): return location if item_to_place.smallkey or item_to_place.bigkey: - location.item = None + location.item = old_item return None diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 84ff1b38..4319d42b 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -116,6 +116,7 @@ class PlacementRule(object): self.needed_keys_w_bk = None self.needed_keys_wo_bk = None self.check_locations_w_bk = None + self.special_bk_avail = False self.check_locations_wo_bk = None self.bk_relevant = True self.key_reduced = False @@ -164,7 +165,10 @@ class PlacementRule(object): def loc_has_bk(l): return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey) - bk_found = any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) + # todo: sometimes the bk avail rule doesn't mean the bk must be avail or this rule is invalid + # but sometimes it certainly does + # check threshold vs len(check_loc) maybe to determine bk isn't relevant? + bk_found = self.special_bk_avail or any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc)) if not bk_found: return True check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk @@ -258,6 +262,8 @@ def analyze_dungeon(key_layout, world, player): return original_key_counter = find_counter({}, False, key_layout, False) + if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None: + original_key_counter = find_counter({}, True, key_layout, False) queue = deque([(None, original_key_counter)]) doors_completed = set() visited_cid = set() @@ -340,6 +346,8 @@ def create_exhaustive_placement_rules(key_layout, world, player): else: placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player) rule.check_locations_w_bk = accessible_loc + if key_layout.big_key_special: + rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None # check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc) else: if big_key_progress(key_counter) and only_sm_doors(key_counter): @@ -1359,6 +1367,13 @@ def check_bk_special(regions, world, player): return False +def forced_big_key_avail(locations): + for loc in locations: + if loc.forced_big_key(): + return loc + return None + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1962,6 +1977,7 @@ def validate_key_placement(key_layout, world, player): found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \ + self_locked_child_door(key_layout, counter) or \ (key_layout.prize_relevant and not counter.prize_doors_opened and found_prize) if not can_progress: missing_locations = set(max_counter.free_locations.keys()).difference(found_locations) @@ -1976,3 +1992,11 @@ def validate_key_placement(key_layout, world, player): return True + +def self_locked_child_door(key_layout, counter): + if len(counter.child_doors) == 1: + door = next(iter(counter.child_doors.keys())) + return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small + return False + + diff --git a/Main.py b/Main.py index d8967f1f..34d35b25 100644 --- a/Main.py +++ b/Main.py @@ -244,7 +244,7 @@ def main(args, seed=None, fish=None): balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom - if not world.can_beat_game(): + if not world.can_beat_game(log_error=True): raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) for player in range(1, world.players+1): From 71079269fa1159f793e8cce2e520aaeb35d54eb3 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 12 Aug 2021 15:59:01 -0600 Subject: [PATCH 051/104] Total keys fix for key logic (Basic reductions do not affect amount of keys placed while crossed does) --- DoorShuffle.py | 4 +++- DungeonGenerator.py | 1 + KeyDoorShuffle.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 67e376f6..918d6192 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1071,6 +1071,7 @@ def assign_cross_keys(dungeon_builders, world, player): # Step 3: Initial valid combination find - reduce flex if needed for name, builder in dungeon_builders.items(): suggested = builder.key_doors_num - builder.key_drop_cnt + builder.total_keys = builder.key_doors_num find_valid_combination(builder, start_regions_map[name], world, player) actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt if actual_chest_keys < suggested: @@ -1087,6 +1088,7 @@ def assign_cross_keys(dungeon_builders, world, player): name = builder.name logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name) builder.key_doors_num += 1 + builder.total_keys = builder.key_doors_num result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False) if result: remaining -= 1 @@ -1380,7 +1382,7 @@ def shuffle_key_doors(builder, world, player): skips.append(world.get_door(dp.door_a, player)) break num_key_doors += 1 - builder.key_doors_num = num_key_doors + builder.key_doors_num = builder.total_keys = num_key_doors find_small_key_door_candidates(builder, start_regions, world, player) find_valid_combination(builder, start_regions, world, player) reassign_key_doors(builder, world, player) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 1c49cba0..06af10da 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1175,6 +1175,7 @@ class DungeonBuilder(object): self.pre_open_stonewalls = set() # used by stonewall system self.candidates = None + self.total_keys = None self.key_doors_num = None self.combo_size = None self.flex = 0 diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 3ce9132f..ca4a7f10 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1387,7 +1387,7 @@ def create_key_counters(key_layout, world, player): state.key_locations = default_key_counts[key_layout.sector.name] else: builder = world.dungeon_layouts[player][key_layout.sector.name] - state.key_locations = builder.key_doors_num - builder.key_drop_cnt + state.key_locations = builder.total_keys - builder.key_drop_cnt state.big_key_special, special_region = False, None for region in key_layout.sector.regions: for location in region.locations: From be6203e3f042f37817fcff567e0a9c366b14b654 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 16 Aug 2021 22:46:26 -0500 Subject: [PATCH 052/104] Moved Crossed OW Shuffle to its own setting, Renamed Tile Swap to Mixed --- BaseClasses.py | 16 +- CLI.py | 5 +- DoorShuffle.py | 2 +- EntranceShuffle.py | 24 +- ItemList.py | 31 +- Main.py | 6 +- Mystery.py | 4 +- OverworldShuffle.py | 570 ++++++++++++++---- README.md | 28 +- Rom.py | 137 ++--- Rules.py | 82 +-- asm/owrando.asm | 2 +- data/base2current.bps | Bin 141167 -> 141164 bytes mystery_example.yml | 10 +- resources/app/cli/args.json | 13 +- resources/app/cli/lang/en.json | 17 +- resources/app/gui/lang/en.json | 7 +- .../app/gui/randomize/overworld/widgets.json | 17 +- source/classes/constants.py | 3 +- source/gui/randomize/overworld.py | 16 +- 20 files changed, 645 insertions(+), 345 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 32028111..a1d522e8 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -19,13 +19,14 @@ from RoomData import Room class World(object): - def __init__(self, players, owShuffle, owSwap, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, + def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): self.players = players self.teams = 1 self.owShuffle = owShuffle.copy() - self.owSwap = owSwap.copy() + self.owCrossed = owCrossed.copy() self.owKeepSimilar = {} + self.owMixed = owMixed.copy() self.owFluteShuffle = {} self.shuffle = shuffle.copy() self.doorShuffle = doorShuffle.copy() @@ -2308,8 +2309,9 @@ class Spoiler(object): 'weapons': self.world.swords, 'goal': self.world.goal, 'ow_shuffle': self.world.owShuffle, - 'ow_swap': self.world.owSwap, + 'ow_crossed': self.world.owCrossed, 'ow_keepsimilar': self.world.owKeepSimilar, + 'ow_mixed': self.world.owMixed, 'ow_fluteshuffle': self.world.owFluteShuffle, 'shuffle': self.world.shuffle, 'door_shuffle': self.world.doorShuffle, @@ -2390,9 +2392,10 @@ class Spoiler(object): outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) - outfile.write('Overworld Tile Swap:'.ljust(line_width) + '%s\n' % self.metadata['ow_swap'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla': + outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_crossed'][player] else 'No')) outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) + outfile.write('Mixed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) @@ -2555,8 +2558,7 @@ class Pot(object): # byte 0: DDOO OEEE (DR, OR, ER) dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} -or_mode = {"vanilla": 0, "parallel": 1, "full": 2} -orswap_mode = {"vanilla": 0, "mixed": 1, "crossed": 1} +or_mode = {"vanilla": 0, "parallel": 1, "full": 1} er_mode = {"vanilla": 0, "simple": 1, "restricted": 3, "full": 3, "crossed": 4, "insanity": 5, "dungeonsfull": 7, "dungeonssimple": 7} # byte 1: LLLW WSSR (logic, mode, sword, retro) @@ -2592,7 +2594,7 @@ class Settings(object): @staticmethod def make_code(w, p): code = bytes([ - (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 4) | (orswap_mode[w.owSwap[p]] << 3) | er_mode[w.shuffle[p]], + (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) | (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0), diff --git a/CLI.py b/CLI.py index fb9367c3..ccc66e00 100644 --- a/CLI.py +++ b/CLI.py @@ -94,7 +94,7 @@ def parse_cli(argv, no_defaults=False): playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', - 'ow_shuffle', 'ow_swap', 'ow_keepsimilar', 'ow_fluteshuffle', + 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_fluteshuffle', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'bombbag', @@ -146,8 +146,9 @@ def parse_settings(): "openpyramid": False, "shuffleganon": True, "ow_shuffle": "vanilla", - "ow_swap": "vanilla", + "ow_crossed": False, "ow_keepsimilar": False, + "ow_mixed": False, "ow_fluteshuffle": "vanilla", "shuffle": "vanilla", "shufflelinks": False, diff --git a/DoorShuffle.py b/DoorShuffle.py index 8ad99965..0a8b33fc 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -214,7 +214,7 @@ def vanilla_key_logic(world, 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) - if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owSwap[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and not world.owCrossed[player] and world.owMixed[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: validate_vanilla_key_logic(world, player) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index dd98a9ed..0863010b 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -50,18 +50,18 @@ def link_entrances(world, player): # inverted entrance mods for owid in swapped_connections.keys(): - if invFlag != (owid in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag != (owid in world.owswaps[player][0] and world.owMixed[player]): for (entrancename, exitname) in swapped_connections[owid]: try: connect_two_way(world, entrancename, exitname, player) except RuntimeError: connect_entrance(world, entrancename, exitname, player) - if invFlag != (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ - invFlag == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag != (0x03 in world.owswaps[player][0] and world.owMixed[player]) and \ + invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]): connect_entrance(world, 'Death Mountain Return Cave (West)', 'Dark Death Mountain Healer Fairy', player) - elif invFlag != (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed') and \ - invFlag == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + elif invFlag != (0x0a in world.owswaps[player][0] and world.owMixed[player]) and \ + invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Bumper Cave (Top)', 'Death Mountain Return Cave Exit (West)', player) # dungeon entrance shuffle @@ -225,7 +225,7 @@ def link_entrances(world, player): random.shuffle(remaining_entrances) old_man_entrance = remaining_entrances.pop() - connect_two_way(world, old_man_entrance if invFlag == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) + connect_two_way(world, old_man_entrance if invFlag == (0x0a in world.owswaps[player][0] and world.owMixed[player]) else 'Bumper Cave (Bottom)', 'Old Man Cave Exit (West)', player) connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player) if invFlag and old_man_exit == 'Spike Cave': bomb_shop_doors.remove('Spike Cave') @@ -791,8 +791,8 @@ def link_entrances(world, player): if not world.shuffle_ganon: connect_two_way(world, 'Ganons Tower' if not invFlag else 'Agahnims Tower', 'Ganons Tower Exit', player) - connect_two_way(world, 'Pyramid Entrance' if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Inverted Pyramid Hole', 'Pyramid', player) + connect_two_way(world, 'Pyramid Entrance' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Entrance', 'Pyramid Exit', player) + connect_entrance(world, 'Pyramid Hole' if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole', 'Pyramid', player) else: caves.extend(['Ganons Tower Exit', 'Pyramid Exit']) hole_targets.append('Pyramid') @@ -804,7 +804,7 @@ def link_entrances(world, player): exit_pool.extend(['Agahnims Tower']) doors.extend(['Agahnims Tower']) - if invFlag == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if invFlag == (0x1b in world.owswaps[player][0] and world.owMixed[player]): exit_pool.extend(['Pyramid Entrance']) hole_entrances.append('Pyramid Hole') entrances_must_exits.append('Pyramid Entrance') @@ -943,7 +943,7 @@ def link_entrances(world, player): world.powder_patch_required[player] = True # check for ganon location - if world.get_entrance('Pyramid Hole' if invFlag == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed') else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': + if world.get_entrance('Pyramid Hole' if invFlag == (0x03 in world.owswaps[player][0] and world.owMixed[player]) else 'Inverted Pyramid Hole', player).connected_region.name != 'Pyramid': world.ganon_at_pyramid[player] = False # check for Ganon's Tower location @@ -1030,7 +1030,7 @@ def scramble_holes(world, player): ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] if not world.shuffle_ganon: - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) else: @@ -1053,7 +1053,7 @@ def scramble_holes(world, player): if world.shuffle_ganon: random.shuffle(hole_targets) exit, target = hole_targets.pop() - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) else: diff --git a/ItemList.py b/ItemList.py index 3d5c3a15..c07da745 100644 --- a/ItemList.py +++ b/ItemList.py @@ -258,7 +258,7 @@ def generate_itempool(world, player): (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray) world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: - (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.owShuffle[player], world.owSwap[player], world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player]) + (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player]) if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] @@ -727,7 +727,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)' 'Rupees (100)': 100, 'Rupees (300)': 300} -def get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic): +def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic): pool = [] placed_items = {} precollected_items = [] @@ -990,21 +990,20 @@ def test(): for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']: for retro in [True, False]: for door_shuffle in ['basic', 'crossed', 'vanilla']: - for owShuffle in ['full', 'parallel', 'vanilla']: - for owSwap in ['vanilla', 'mixed', 'crossed']: - out = get_pool_core(progressive, owShuffle, owSwap, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) - count = len(out[0]) + len(out[1]) + for bombbag in [True, False]: + out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic) + count = len(out[0]) + len(out[1]) - correct_count = total_items_to_place - if goal == 'pedestal' and swords != 'vanilla': - # pedestal goals generate one extra item - correct_count += 1 - if retro: - correct_count += 28 - try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) - except AssertionError as e: - print(e) + correct_count = total_items_to_place + if goal == 'pedestal' and swords != 'vanilla': + # pedestal goals generate one extra item + correct_count += 1 + if retro: + correct_count += 28 + try: + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag)) + except AssertionError as e: + print(e) if __name__ == '__main__': test() diff --git a/Main.py b/Main.py index 84d97fb9..b7f0c295 100644 --- a/Main.py +++ b/Main.py @@ -61,7 +61,7 @@ def main(args, seed=None, fish=None): for player, code in args.code.items(): if code: Settings.adjust_args_from_code(code, player, args) - world = World(args.multi, args.ow_shuffle, args.ow_swap, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, + world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) logger = logging.getLogger('') @@ -263,7 +263,7 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owSwap[1] != 'vanilla' or str(world.seed).startswith('M'): + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] or world.owMixed[1] or str(world.seed).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' @@ -369,7 +369,7 @@ def main(args, seed=None, fish=None): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.owShuffle, world.owSwap, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, + ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) ret.teams = world.teams diff --git a/Mystery.py b/Mystery.py index 88f5d7f0..f92764d9 100644 --- a/Mystery.py +++ b/Mystery.py @@ -133,9 +133,9 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' - overworld_swap = get_choice('overworld_swap') - ret.ow_swap = overworld_swap if overworld_swap != 'none' else 'vanilla' + ret.ow_crossed = get_choice('overworld_crossed') ret.ow_keepsimilar = get_choice('overworld_keepsimilar') + ret.ow_mixed = get_choice('overworld_mixed') overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' entrance_shuffle = get_choice('entrance_shuffle') diff --git a/OverworldShuffle.py b/OverworldShuffle.py index d1a3784f..5d1d9669 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -11,9 +11,25 @@ def link_overworld(world, player): for exitname, destname in temporary_mandatory_connections: connect_two_way(world, exitname, destname, player) - # tile shuffle trimmed_groups = copy.deepcopy(OWEdgeGroups) - if world.owSwap[player] != 'vanilla': + + # adjust Frog/Dig Game swap manually due to NP/P relationship with LW + if world.owShuffle[player] == 'parallel' and not world.owKeepSimilar[player]: + for group in trimmed_groups.keys(): + (std, region, axis, terrain, _, _) = group + (forward_edges, back_edges) = trimmed_groups[group] + if ['Dig Game EC', 'Dig Game ES'] in forward_edges: + forward_edges = list(filter((['Dig Game EC', 'Dig Game ES']).__ne__, forward_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1)][0].append(['Dig Game ES']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][0].append(['Dig Game EC']) + if ['Frog WC', 'Frog WS'] in back_edges: + back_edges = list(filter((['Frog WC', 'Frog WS']).__ne__, back_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1)][1].append(['Frog WS']) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][1].append(['Frog WC']) + trimmed_groups[group] = (forward_edges, back_edges) + + # tile shuffle + if world.owMixed[player]: tile_groups = {} for (name, groupType) in OWTileGroups.keys(): if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: @@ -44,7 +60,7 @@ def link_overworld(world, player): exist_dw_regions.extend(OWTileRegions.inverse[owid]) tile_groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) - #tile shuffle happens here, the groups that remain in the list are the tiles that get swapped + # tile shuffle happens here, the groups that remain in the list are the tiles that get swapped removed = list() for group in tile_groups.keys(): if random.randint(0, 1): @@ -52,7 +68,7 @@ def link_overworld(world, player): for group in removed: tile_groups.pop(group, None) - #save shuffled tiles to world object + # save shuffled tiles to world object for group in tile_groups.keys(): (owids, lw_regions, dw_regions) = tile_groups[group] (exist_owids, exist_lw_regions, exist_dw_regions) = world.owswaps[player] @@ -61,7 +77,7 @@ def link_overworld(world, player): exist_dw_regions.extend(dw_regions) world.owswaps[player] = [exist_owids, exist_lw_regions, exist_dw_regions] - #replace LW edges with DW + # replace LW edges with DW ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool for edgeset in temporary_mandatory_connections: for edge in edgeset: @@ -108,7 +124,7 @@ def link_overworld(world, player): #TODO: Figure out a way to handle index changes on the fly when removing items logging.getLogger('').warning('OW Tile Swap encountered minor IndexError... retrying') - if 0x28 in world.owswaps[player][0]: #handle Frog/Dig Game swap manually due to NP/P relationship with LW + if world.owShuffle[player] != 'parallel' and 0x28 in world.owswaps[player][0]: # handle Frog/Dig Game swap manually due to NP/P relationship with LW trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][0].append(['Maze Race ES']) trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][1].append(['Kakariko Suburb WS']) trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][0].remove(['Maze Race ES']) @@ -139,7 +155,7 @@ def link_overworld(world, player): # make new connections for owid in ow_connections.keys(): - if (world.mode[player] == 'inverted') == (owid in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (owid in world.owswaps[player][0] and world.owMixed[player]): for (exitname, regionname) in ow_connections[owid][0]: connect_simple(world, exitname, regionname, player) else: @@ -151,66 +167,99 @@ def link_overworld(world, player): connect_custom(world, connected_edges, player) # layout shuffle - if world.owShuffle[player] == 'vanilla': + if world.owShuffle[player] == 'vanilla' and not world.owCrossed[player]: + # vanilla transitions groups = list(trimmed_groups.values()) for (forward_edge_sets, back_edge_sets) in groups: assert len(forward_edge_sets) == len(back_edge_sets) for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): assert len(forward_set) == len(back_set) for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player) - connected_edges.append(forward_edge) - connected_edges.append(back_edge) + connect_two_way(world, forward_edge, back_edge, player, connected_edges) assert len(connected_edges) == len(default_connections) * 2, connected_edges else: if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': for exitname, destname in parallelsimilar_connections: - connect_two_way(world, exitname, destname, player) - connected_edges.append(exitname) - connected_edges.append(destname) + connect_two_way(world, exitname, destname, player, connected_edges) + + if world.owShuffle[player] == 'vanilla' and world.owCrossed[player]: + if world.mode[player] == 'standard': + # connect vanilla std + for group in trimmed_groups.keys(): + (std, _, _, _, _, _) = group + if std == OpenStd.Standard: + (forward_set, back_set) = trimmed_groups[group] + for (forward_edges, back_edges) in zip(forward_set, back_set): + for (forward_edge, back_edge) in zip(forward_edges, back_edges): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + + # connect non-parallel edges + for group in trimmed_groups.keys(): + (_, _, _, _, parallel, _) = group + if parallel == IsParallel.No: + (forward_set, back_set) = trimmed_groups[group] + for (forward_edges, back_edges) in zip(forward_set, back_set): + for (forward_edge, back_edge) in zip(forward_edges, back_edges): + if forward_edge not in connected_edges and back_edge not in connected_edges: + connect_two_way(world, forward_edge, back_edge, player, connected_edges) #TODO: Remove, just for testing for exitname, destname in test_connections: - connect_two_way(world, exitname, destname, player) - connected_edges.append(exitname) - connected_edges.append(destname) + connect_two_way(world, exitname, destname, player, connected_edges) trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) groups = reorganize_groups(world, trimmed_groups, player) - #all shuffling occurs here - random.shuffle(groups) - for (forward_edge_sets, back_edge_sets) in groups: - assert len(forward_edge_sets) == len(back_edge_sets) - random.shuffle(back_edge_sets) - for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): - assert len(forward_set) == len(back_set) - for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player) - connected_edges.append(forward_edge) - connected_edges.append(back_edge) - if world.owShuffle[player] == 'parallel': - if forward_edge in parallel_links.keys() or forward_edge in parallel_links.inverse.keys(): - try: - parallel_forward_edge = parallel_links[forward_edge] if forward_edge in parallel_links.keys() else parallel_links.inverse[forward_edge][0] - parallel_back_edge = parallel_links[back_edge] if back_edge in parallel_links.keys() else parallel_links.inverse[back_edge][0] - connect_two_way(world, parallel_forward_edge, parallel_back_edge, player) - connected_edges.append(parallel_forward_edge) - connected_edges.append(parallel_back_edge) - except KeyError: - # TODO: Figure out why non-parallel edges are getting into parallel groups - raise KeyError('No parallel edge for edge %d' % back_edge) + # all layout shuffling occurs here + if world.owShuffle[player] != 'vanilla': + # layout shuffle + random.shuffle(groups) + for (forward_edge_sets, back_edge_sets) in groups: + assert len(forward_edge_sets) == len(back_edge_sets) + random.shuffle(forward_edge_sets) + random.shuffle(back_edge_sets) + if len(forward_edge_sets) > 0: + f = 0 + b = 0 + while f < len(forward_edge_sets) and b < len(back_edge_sets): + forward_set = forward_edge_sets[f] + back_set = back_edge_sets[b] + while forward_set[0] in connected_edges: + f += 1 + if f < len(forward_edge_sets): + forward_set = forward_edge_sets[f] + f += 1 + while back_set[0] in connected_edges: + b += 1 + if b < len(back_edge_sets): + back_set = back_edge_sets[b] + b += 1 + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + else: + # vanilla/crossed shuffle + for (forward_edge_sets, back_edge_sets) in groups: + assert len(forward_edge_sets) == len(back_edge_sets) + for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): + assert len(forward_set) == len(back_set) + swapped = random.randint(0, 1) + for (forward_edge, back_edge) in zip(forward_set, back_set): + if forward_edge not in connected_edges and back_edge not in connected_edges: + if swapped: + forward_edge = parallel_links[forward_edge] if forward_edge in parallel_links else parallel_links.inverse[forward_edge][0] + connect_two_way(world, forward_edge, back_edge, player, connected_edges) assert len(connected_edges) == len(default_connections) * 2, connected_edges - + # flute shuffle def connect_flutes(flute_destinations): for o in range(0, len(flute_destinations)): owslot = flute_destinations[o] regions = flute_data[owslot][0] - if (world.mode[player] == 'inverted') == (flute_data[owslot][1] in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (flute_data[owslot][1] in world.owswaps[player][0] and world.owMixed[player]): connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player) else: connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player) @@ -232,7 +281,7 @@ def link_overworld(world, player): new_ignored.add(exit.connected_region.name) getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name]) - if (world.mode[player] == 'inverted') == (flute_data[owid][1] in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (flute_data[owid][1] in world.owswaps[player][0] and world.owMixed[player]): new_region = flute_data[owid][0][0] else: new_region = flute_data[owid][0][1] @@ -269,18 +318,15 @@ def link_overworld(world, player): world.owflutespots[player] = new_spots connect_flutes(new_spots) - def connect_custom(world, connected_edges, player): if hasattr(world, 'custom_overworld') and world.custom_overworld[player]: for edgename1, edgename2 in world.custom_overworld[player]: - connect_two_way(world, edgename1, edgename2, player) - connected_edges.append(edgename1) - connected_edges.append(edgename2) + connect_two_way(world, edgename1, edgename2, player, connected_edges) def connect_simple(world, exitname, regionname, player): world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) -def connect_two_way(world, edgename1, edgename2, player): +def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): edge1 = world.get_entrance(edgename1, player) edge2 = world.get_entrance(edgename2, player) @@ -302,23 +348,41 @@ def connect_two_way(world, edgename1, edgename2, player): x.dest = y y.dest = x - if world.owShuffle[player] != 'vanilla' or world.owSwap[player] != 'vanilla': + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player]: world.spoiler.set_overworld(edgename2, edgename1, 'both', player) + if connected_edges is not None: + connected_edges.append(edgename1) + connected_edges.append(edgename2) + + # connecting parallel connections + if world.owShuffle[player] == 'parallel' or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player]): + if (edgename1 in parallel_links.keys() or edgename1 in parallel_links.inverse.keys()): + try: + parallel_forward_edge = parallel_links[edgename1] if edgename1 in parallel_links.keys() else parallel_links.inverse[edgename1][0] + parallel_back_edge = parallel_links[edgename2] if edgename2 in parallel_links.keys() else parallel_links.inverse[edgename2][0] + if not (parallel_forward_edge in connected_edges) and not (parallel_back_edge in connected_edges): + connect_two_way(world, parallel_forward_edge, parallel_back_edge, player, connected_edges) + except KeyError: + # TODO: Figure out why non-parallel edges are getting into parallel groups + raise KeyError('No parallel edge for edge %s' % edgename2) + def remove_reserved(world, groupedlist, connected_edges, player): new_grouping = {} for group in groupedlist.keys(): new_grouping[group] = ([], []) for group in groupedlist.keys(): - (std, region, axis, terrain, parallel, count) = group + (_, region, _, _, _, _) = group (forward_edges, back_edges) = groupedlist[group] + # remove edges already connected (thru plando and other forced connections) for edge in connected_edges: forward_edges = list(list(filter((edge).__ne__, i)) for i in forward_edges) back_edges = list(list(filter((edge).__ne__, i)) for i in back_edges) - - if world.owShuffle[player] == 'parallel' and region == WorldType.Dark: + + # remove parallel edges from pool, since they get added during shuffle + if (not world.owCrossed[player] and world.owShuffle[player] == 'parallel') and region == WorldType.Dark: for edge in parallel_links: forward_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in forward_edges) back_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in back_edges) @@ -329,8 +393,6 @@ def remove_reserved(world, groupedlist, connected_edges, player): forward_edges = list(filter(([]).__ne__, forward_edges)) back_edges = list(filter(([]).__ne__, back_edges)) - #TODO: Remove edges set in connect_custom. The lists above can be left with invalid counts if connect_custom removes entries, they need to get put into their appropriate group - (exist_forward_edges, exist_back_edges) = new_grouping[group] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) @@ -340,11 +402,324 @@ def remove_reserved(world, groupedlist, connected_edges, player): return new_grouping def reorganize_groups(world, groups, player): + # predefined shuffle groups get reorganized here + # this restructures the candidate pool based on the chosen settings if world.owShuffle[player] == 'full': - #predefined shuffle groups get reorganized here + if world.owCrossed[player]: + if world.owKeepSimilar[player]: + if world.mode[player] == 'standard': + # tuple goes to (A,_,C,D,_,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, _, axis, terrain, _, count) = group + new_grouping[(std, axis, terrain, count)] = ([], []) + + for group in grouping.keys(): + (std, _, axis, terrain, _, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,_,C,D,_,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, _, axis, terrain, _, count) = group + new_grouping[(axis, terrain, count)] = ([], []) + + for group in grouping.keys(): + (_, _, axis, terrain, _, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(axis, terrain, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.mode[player] == 'standard': + # tuple goes to (A,_,C,D,_,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, _, axis, terrain, _, _) = group + new_grouping[(std, axis, terrain)] = ([], []) + + for group in grouping.keys(): + (std, _, axis, terrain, _, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, axis, terrain)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,_,C,D,_,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, _, axis, terrain, _, _) = group + new_grouping[(axis, terrain)] = ([], []) + + for group in grouping.keys(): + (_, _, axis, terrain, _, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(axis, terrain)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.owKeepSimilar[player]: + if world.mode[player] == 'standard': + # tuple goes to (A,B,C,D,_,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, region, axis, terrain, _, count) = group + new_grouping[(std, region, axis, terrain, count)] = ([], []) + + for group in grouping.keys(): + (std, region, axis, terrain, _, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,B,C,D,_,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, region, axis, terrain, _, count) = group + new_grouping[(region, axis, terrain, count)] = ([], []) + + for group in grouping.keys(): + (_, region, axis, terrain, _, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.mode[player] == 'standard': + # tuple goes to (A,B,C,D,_,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, region, axis, terrain, _, _) = group + new_grouping[(std, region, axis, terrain)] = ([], []) + + for group in grouping.keys(): + (std, region, axis, terrain, _, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, region, axis, terrain)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,B,C,D,_,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, region, axis, terrain, _, _) = group + new_grouping[(region, axis, terrain)] = ([], []) + + for group in grouping.keys(): + (_, region, axis, terrain, _, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(region, axis, terrain)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + elif world.owShuffle[player] == 'parallel': + if world.owCrossed[player]: + if world.owKeepSimilar[player]: + if world.mode[player] == 'standard': + # tuple goes to (A,_,C,D,E,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, _, axis, terrain, parallel, count) = group + new_grouping[(std, axis, terrain, parallel, count)] = ([], []) + + for group in grouping.keys(): + (std, _, axis, terrain, parallel, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,_,C,D,E,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, _, axis, terrain, parallel, count) = group + new_grouping[(axis, terrain, parallel, count)] = ([], []) + + for group in grouping.keys(): + (_, _, axis, terrain, parallel, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.mode[player] == 'standard': + # tuple goes to (A,_,C,D,E,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, _, axis, terrain, parallel, _) = group + new_grouping[(std, axis, terrain, parallel)] = ([], []) + + for group in grouping.keys(): + (std, _, axis, terrain, parallel, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,_,C,D,E,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, _, axis, terrain, parallel, _) = group + new_grouping[(axis, terrain, parallel)] = ([], []) + + for group in grouping.keys(): + (_, _, axis, terrain, parallel, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.owKeepSimilar[player]: + if world.mode[player] == 'standard': + # tuple stays (A,B,C,D,E,F) + for grouping in (groups,): + return list(grouping.values()) + else: + # tuple goes to (_,B,C,D,E,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, count) = group + new_grouping[(region, axis, terrain, parallel, count)] = ([], []) + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(region, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.mode[player] == 'standard': + # tuple goes to (A,B,C,D,E,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, region, axis, terrain, parallel, _) = group + new_grouping[(std, region, axis, terrain, parallel)] = ([], []) + + for group in grouping.keys(): + (std, region, axis, terrain, parallel, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, parallel)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,B,C,D,E,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, _) = group + new_grouping[(region, axis, terrain, parallel)] = ([], []) + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + elif world.owShuffle[player] == 'vanilla' and world.owCrossed[player]: if world.owKeepSimilar[player]: if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,_,F) + # tuple goes to (A,B,C,D,_,F) for grouping in (groups,): new_grouping = {} @@ -362,7 +737,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: - #tuple goes to (_,B,C,D,_,F) + # tuple goes to (_,B,C,D,_,F) for grouping in (groups,): new_grouping = {} @@ -381,7 +756,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,_,_) + # tuple goes to (A,B,C,D,_,_) for grouping in (groups,): new_grouping = {} @@ -402,7 +777,7 @@ def reorganize_groups(world, groups, player): return list(new_grouping.values()) else: - #tuple goes to (_,B,C,D,_,_) + # tuple goes to (_,B,C,D,_,_) for grouping in (groups,): new_grouping = {} @@ -421,82 +796,14 @@ def reorganize_groups(world, groups, player): exist_back_edges.extend(back_edges) new_grouping[(region, axis, terrain)] = (exist_forward_edges, exist_back_edges) - return list(new_grouping.values()) - elif world.owShuffle[player] == 'parallel': - #predefined shuffle groups get reorganized here - if world.owKeepSimilar[player]: - if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,E,F) - for grouping in (groups,): - return list(grouping.values()) - else: - #tuple goes to (_,B,C,D,E,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, count) = group - new_grouping[(region, axis, terrain, parallel, count)] = ([], []) - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.mode[player] == 'standard': - #tuple stays (A,B,C,D,E,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, region, axis, terrain, parallel, _) = group - new_grouping[(std, region, axis, terrain, parallel)] = ([], []) - - for group in grouping.keys(): - (std, region, axis, terrain, parallel, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, parallel)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - #tuple goes to (_,B,C,D,E,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, _) = group - new_grouping[(region, axis, terrain, parallel)] = ([], []) - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) - return list(new_grouping.values()) else: raise NotImplementedError('Shuffling not supported yet') def create_flute_exits(world, player): for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): - if (world.owSwap[player] != 'mixed' and region.type == RegionType.LightWorld) \ - or (world.owSwap[player] == 'mixed' and region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ + if (not world.owMixed[player] and region.type == RegionType.LightWorld) \ + or (world.owMixed[player] and region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) @@ -506,7 +813,7 @@ def create_flute_exits(world, player): world.initialize_regions() def update_world_regions(world, player): - if world.owSwap[player] == 'mixed': + if world.owMixed[player]: for name in world.owswaps[player][1]: world.get_region(name, player).type = RegionType.DarkWorld for name in world.owswaps[player][2]: @@ -1110,9 +1417,8 @@ ow_connections = { } parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'), - ('Dig Game EC', 'Frog WC'), - ('Dig Game ES', 'Frog WS') - ] + ('Dig Game EC', 'Frog WC') + ] # non shuffled overworld default_connections = [#('Lost Woods NW', 'Master Sword Meadow SC'), diff --git a/README.md b/README.md index 3ecdb94f..7bf01573 100644 --- a/README.md +++ b/README.md @@ -66,19 +66,9 @@ OW Transitions are shuffled, but both worlds will have a matching layout. OW Transitions are shuffled within each world separately. -## Overworld Tile Swap (--ow_swap) +## Crossed (--ow_crossed) -### Vanilla - -OW tiles remain in their original worlds. - -### Mixed - -OW tiles are randomly chosen to become a part of the opposite world - -### Crossed - -OW tiles remain in their original world, but transitions can now be travel cross-world. +This allows OW connections to be shuffled cross-world. ## Visual Representation of Main OW Shuffle Settings @@ -88,6 +78,10 @@ OW tiles remain in their original world, but transitions can now be travel cross This keeps similar edge transitions together. ie. The 2 west edges of Potion Shop will be paired to another set of two similar edges +## Mixed Overworld (--ow_mixed) + +OW tiles are randomly chosen to become a part of the opposite world + ## Flute Shuffle (--ow_fluteshuffle) When enabled, new flute spots are generated and gives the player the option to cancel out of the flute menu by pressing X. @@ -120,10 +114,10 @@ Show the help message and exit. For specifying the overworld layout shuffle you want as above. (default: vanilla) ``` ---ow_swap +--ow_crossed ``` -For specifying the overworld tile swap you want as above. (default: vanilla) +This allows cross-world connections on the overworld ``` --ow_keepsimilar @@ -131,6 +125,12 @@ For specifying the overworld tile swap you want as above. (default: vanilla) This keeps similar edge transitions paired together with other pairs of transitions +``` +--ow_mixed +``` + +This gives each OW tile a random chance to be swapped to the opposite world + ``` --ow_fluteshuffle ``` diff --git a/Rom.py b/Rom.py index 217582c1..26911e9f 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'cc8fc59caa0bbe6d26ac64b9d2893709' +RANDOMIZERBASEHASH = '99f3f57ab2c9449172cade4927a462d6' class JsonRom(object): @@ -612,71 +612,20 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.pot_contents[player]: write_pots_to_rom(rom, world.pot_contents[player]) - # patch overworld edges - inverted_buffer = [0] * 0x82 - if world.owShuffle[player] != 'vanilla' or world.owSwap[player] != 'vanilla': - owMode = 0 - if world.owShuffle[player] == 'parallel': - owMode = 1 - elif world.owShuffle[player] == 'full': - owMode = 2 - - if world.owSwap[player] == 'mixed': - owMode |= 0x100 - world.fix_fake_world[player] = True - elif world.owSwap[player] == 'crossed': - owMode |= 0x200 - world.fix_fake_world[player] = True - - write_int16(rom, 0x150002, owMode) - - owFlags = 0 - if world.owKeepSimilar[player]: - owFlags |= 0x1 - if world.owFluteShuffle[player] != 'vanilla': - owFlags |= 0x100 - - write_int16(rom, 0x150004, owFlags) - - rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves - - # patches map data specific for OW Shuffle - #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM - inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x1B] = inverted_buffer[0x1B] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x22] = inverted_buffer[0x22] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x3F] = inverted_buffer[0x3F] | 0x2 # added C to terrain - #inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM - inverted_buffer[0x5A] = inverted_buffer[0x5A] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x5B] = inverted_buffer[0x5B] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x62] = inverted_buffer[0x62] | 0x2 # rocks added to prevent OWG hardlock - inverted_buffer[0x7F] = inverted_buffer[0x7F] | 0x2 # added C to terrain - - if world.owSwap[player] == 'mixed': - for b in world.owswaps[player][0]: - # load inverted maps - inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) - - # set world flag - rom.write_byte(0x153A00 + b, 0x00 if b >= 0x40 else 0x40) - - for edge in world.owedges: - if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: - write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) - write_int16(rom, edge.getAddress() + 0x0e, edge.getTarget()) - # patch flute spots + owFlags = 0 if world.owFluteShuffle[player] == 'vanilla': flute_spots = default_flute_connections else: flute_spots = world.owflutespots[player] + owFlags |= 0x100 for o in range(0, len(flute_spots)): owslot = flute_spots[o] offset = 0 data = flute_data[owslot] - if (world.mode[player] == 'inverted') != (data[1] in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (data[1] in world.owswaps[player][0] and world.owMixed[player]): offset = 0x40 write_int16(rom, snes_to_pc(0x02E849 + (o * 2)), data[1] + offset) # owid @@ -695,7 +644,55 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(snes_to_pc(0x0AB78B + o), data[12] // 0x100) # X high byte rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # Y low byte rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # Y high byte + + # patch overworld edges + inverted_buffer = [0] * 0x82 + if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] or world.owMixed[player]: + owMode = 0 + if world.owShuffle[player] == 'parallel': + owMode = 1 + elif world.owShuffle[player] == 'full': + owMode = 2 + + if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player]): + owMode |= 0x100 + if world.owCrossed[player]: + owMode |= 0x200 + world.fix_fake_world[player] = True + if world.owMixed[player]: + owMode |= 0x400 + write_int16(rom, 0x150002, owMode) + + write_int16(rom, 0x150004, owFlags) + + rom.write_byte(0x18004C, 0x01) # patch for allowing Frogsmith to enter multi-entrance caves + + # patches map data specific for OW Shuffle + #inverted_buffer[0x03] = inverted_buffer[0x03] | 0x2 # convenient portal on WDM + inverted_buffer[0x1A] = inverted_buffer[0x1A] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x1B] = inverted_buffer[0x1B] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x22] = inverted_buffer[0x22] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x3F] = inverted_buffer[0x3F] | 0x2 # added C to terrain + #inverted_buffer[0x43] = inverted_buffer[0x43] | 0x2 # convenient portal on WDDM + inverted_buffer[0x5A] = inverted_buffer[0x5A] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x5B] = inverted_buffer[0x5B] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x62] = inverted_buffer[0x62] | 0x2 # rocks added to prevent OWG hardlock + inverted_buffer[0x7F] = inverted_buffer[0x7F] | 0x2 # added C to terrain + + if world.owMixed[player]: + for b in world.owswaps[player][0]: + # load inverted maps + inverted_buffer[b] = (inverted_buffer[b] & 0xFE) | ((inverted_buffer[b] + 1) % 2) + + # set world flag + rom.write_byte(0x153A00 + b, 0x00 if b >= 0x40 else 0x40) + + for edge in world.owedges: + if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player: + write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) + write_int16(rom, edge.getAddress() + 0x0e, edge.getTarget()) + # patch entrance/exits/holes for region in world.regions: @@ -2177,7 +2174,7 @@ def write_strings(rom, world, player, team): if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: - if world.mode[player] == 'inverted' != (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if world.mode[player] == 'inverted' != (0x1b in world.owswaps[player][0] and world.owMixed[player]): entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) else: entrances_to_hint.update({'Pyramid Entrance': 'The pyramid ledge'}) @@ -2247,7 +2244,7 @@ def write_strings(rom, world, player, team): tt[hint_locations.pop(0)] = this_hint # Adding a guaranteed hint for the Flute in overworld shuffle. - if world.owShuffle[player] in ['parallel','full']: + if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: this_location = world.find_items_not_key_only('Ocarina', player) if this_location: this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' @@ -2255,7 +2252,7 @@ def write_strings(rom, world, player, team): # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. items_to_hint = RelevantItems.copy() - if world.owShuffle[player] in ['parallel','full']: + if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: items_to_hint.remove('Ocarina') if world.keyshuffle[player]: items_to_hint.extend(SmallKeys) @@ -2264,7 +2261,7 @@ def write_strings(rom, world, player, team): random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0 - hint_count += 1 if world.owShuffle[player] in ['parallel', 'full'] else 0 + hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owMixed[player] else 0 while hint_count > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -2475,7 +2472,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): if world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: write_int16(rom, 0x15AEE + 2*0x38, 0x00E0) write_int16(rom, 0x15AEE + 2*0x25, 0x000C) - if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owMixed[player]): if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) # mountain cave starts on OW @@ -2498,18 +2495,18 @@ def set_inverted_mode(world, player, rom, inverted_buffer): rom.write_byte(snes_to_pc(0x02D9B8), 0x12) rom.write_bytes(0x180247, [0x00, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00]) #indicates the overworld door being used for the single entrance spawn point - if (world.mode[player] == 'inverted') != (0x05 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x05 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC655), [0x4A, 0x1D, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x07 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x07 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC387), [0xDD, 0xD1]) # add warps under rocks rom.write_bytes(snes_to_pc(0x1BD1DD), [0xA4, 0x06, 0x82, 0x9E, 0x06, 0x82, 0xFF, 0xFF]) # add warps under rocks rom.write_byte(0x180089, 0x01) # open TR after exit rom.write_bytes(0x0086E, [0x5C, 0x00, 0xA0, 0xA1]) # TR tail if world.shuffle[player] in ['vanilla']: world.fix_trock_doors[player] = True - if (world.mode[player] == 'inverted') != (0x10 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x10 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC67A), [0x2E, 0x0B, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x1B in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x1B in world.owswaps[player][0] and world.owMixed[player]): write_int16(rom, 0x15AEE + 2 * 0x06, 0x0020) # post aga hyrule castle spawn rom.write_byte(0x15B8C + 0x06, 0x1B) write_int16(rom, 0x15BDB + 2 * 0x06, 0x00AE) @@ -2597,21 +2594,21 @@ def set_inverted_mode(world, player, rom, inverted_buffer): write_int16(rom, 0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door write_int16(rom, 0xDBA71 + 2 * 0x35, 0x011C) - if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) # frog pickup on contact - if (world.mode[player] == 'inverted') != (0x2C in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x2C in world.owswaps[player][0] and world.owMixed[player]): if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: rom.write_byte(0x15B8C, 0x6C) # exit links at bomb shop area rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house rom.write_byte(0xDBB73 + 0x52, 0x01) - if (world.mode[player] == 'inverted') != (0x2F in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x2F in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC80D), [0xB2, 0x0B, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC81E), [0x94, 0x1D, 0x82]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x33 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x33 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC3DF), [0xD8, 0xD1]) # add warp under rock rom.write_bytes(snes_to_pc(0x1BD1D8), [0xA8, 0x02, 0x82, 0xFF, 0xFF]) # add warp under rock - if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owMixed[player]): rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82]) # add warp under rock # apply inverted map changes diff --git a/Rules.py b/Rules.py index 3cd00a14..3e50c6d4 100644 --- a/Rules.py +++ b/Rules.py @@ -844,7 +844,7 @@ def ow_rules(world, player): else: set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) - if (world.mode[player] == 'inverted') == (0x00 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x00 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lost Woods East Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pedestal Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -859,12 +859,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Skull Woods Forgotten (Middle) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Skull Woods Front Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x02 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x02 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lumberjack Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Lumberjack Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('West Death Mountain (Top) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -872,7 +872,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Bubble Boy Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('West Dark Death Mountain (Bottom) Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('East Death Mountain (Top West) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('East Death Mountain (Top East) Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -894,7 +894,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Dark Floating Island Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player)) - if (world.mode[player] == 'inverted') == (0x07 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x07 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('TR Pegs Area Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('TR Pegs Teleporter', player), lambda state: state.has('Hammer', player)) else: @@ -902,7 +902,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Turtle Rock Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) - if (world.mode[player] == 'inverted') == (0x0a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x0a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Mountain Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Mountain Entry Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Mountain Entry Entrance Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -911,12 +911,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Bumper Cave Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x0f in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x0f in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Zora Waterfall Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Catfish Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x10 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x10 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lost Woods Pass West Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Top Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lost Woods Pass East Bottom Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -929,24 +929,24 @@ def ow_rules(world, player): set_rule(world.get_entrance('West Dark World Teleporter (Hammer)', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) set_rule(world.get_entrance('West Dark World Teleporter (Rock)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player)) # bunny cannot lift bushes - if (world.mode[player] == 'inverted') == (0x11 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x11 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Fortune Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Outcast Fortune Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x12 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x12 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Pond Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Outcast Pond Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x13 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x13 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Sanctuary Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Bonk Rock Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Chapel Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Chapel Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x14 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x14 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has_Pearl(player) and state.has_Mirror(player)) set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has_Pearl(player) and state.has_Mirror(player)) else: @@ -956,28 +956,28 @@ def ow_rules(world, player): set_rule(world.get_entrance('Dark Graveyard Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Graveyard Grave Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x15 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x15 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('River Bend Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('River Bend East Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Qirn Jump Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Qirn Jump East Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x16 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x16 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Potion Shop Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Potion Shop Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Witch Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Witch Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x17 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x17 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Zora Approach Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Zora Approach Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Catfish Approach Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Catfish Approach Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x18 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x18 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Kakariko Grass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -985,13 +985,13 @@ def ow_rules(world, player): set_rule(world.get_entrance('Village of Outcasts Southwest Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hammer House Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x1a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Forgotton Forest Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Forgotton Forest Fence Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Shield Shop Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x1b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1b in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: False) set_rule(world.get_entrance('Inverted Pyramid Entrance', player), lambda state: False) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player]) @@ -1017,7 +1017,7 @@ def ow_rules(world, player): set_rule(world.get_entrance('Pyramid Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Post Aga Inverted Teleporter', player), lambda state: state.has('Beat Agahnim 1', player)) - if (world.mode[player] == 'inverted') == (0x1d in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1d in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Wooden Bridge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Wooden Bridge Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Wooden Bridge West Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1026,12 +1026,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Broken Bridge East Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Broken Bridge Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x1e in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x1e in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Eastern Palace Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Palace of Darkness Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x22 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x22 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Blacksmith Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Blacksmith Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Bat Cave Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1039,19 +1039,19 @@ def ow_rules(world, player): set_rule(world.get_entrance('Hammer Pegs Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hammer Pegs Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x25 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x25 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Sand Dunes Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Dunes Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x28 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x28 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Maze Race Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Maze Race Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dig Game Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dig Game Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x29 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x29 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Kakariko Suburb Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Kakariko Suburb South Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -1059,24 +1059,24 @@ def ow_rules(world, player): set_rule(world.get_entrance('Frog Prison Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Archery Game Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Flute Boy Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Flute Boy Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Stumpy Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Stumpy Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2b in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Central Bonk Rocks Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Bonk Rocks Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2c in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2c in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Links House Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Big Bomb Shop Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2d in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2d in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Stone Bridge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Stone Bridge South Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hobo Mirror Spot', player), lambda state: state.has_Mirror(player) and state.has_Pearl(player) and state.has('Flippers', player)) @@ -1085,19 +1085,19 @@ def ow_rules(world, player): set_rule(world.get_entrance('Hammer Bridge South Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Dark Hobo Mirror Spot', player), lambda state: state.has_Mirror(player) and state.has_Pearl(player) and state.has('Flippers', player)) - if (world.mode[player] == 'inverted') == (0x2e in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2e in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Tree Line Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark Tree Line Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x2f in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x2f in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Eastern Nook Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot use hammer else: set_rule(world.get_entrance('Darkness Nook Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) - if (world.mode[player] == 'inverted') == (0x30 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x30 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Checkerboard Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Desert Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -1112,14 +1112,14 @@ def ow_rules(world, player): set_rule(world.get_entrance('Misery Mire Main Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Misery Mire Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - if (world.mode[player] == 'inverted') == (0x32 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x32 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Cave 45 Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Flute Boy Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Stumpy Approach Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Stumpy Bush Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x33 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x33 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('C Whirlpool Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('C Whirlpool Outer Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) # bunny cannot use hammer @@ -1130,12 +1130,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has_Pearl(player)) set_rule(world.get_entrance('South Teleporter Cliff Ledge Drop', player), lambda state: state.can_lift_rocks(player) and state.has_Pearl(player)) - if (world.mode[player] == 'inverted') == (0x34 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x34 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Statues Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Hype Cave Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x35 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x35 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Lake Hylia Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lake Hylia Northeast Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Lake Hylia Island Mirror Spot', player), lambda state: state.has_Mirror(player) and state.has_Pearl(player) and state.has('Flippers', player)) @@ -1152,12 +1152,12 @@ def ow_rules(world, player): set_rule(world.get_entrance('Ice Palace Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Ice Palace Teleporter', player), lambda state: state.can_lift_heavy_rocks(player)) - if (world.mode[player] == 'inverted') == (0x37 in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x37 in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Ice Cave Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Shopping Mall Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3a in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3a in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Desert Pass Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Desert Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: @@ -1165,17 +1165,17 @@ def ow_rules(world, player): set_rule(world.get_entrance('Swamp Nook Southeast Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Swamp Nook Pegs Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3b in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3b in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Dam Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Swamp Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3c in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3c in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('South Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Dark South Pass Mirror Spot', player), lambda state: state.has_Mirror(player)) - if (world.mode[player] == 'inverted') == (0x3f in world.owswaps[player][0] and world.owSwap[player] == 'mixed'): + if (world.mode[player] == 'inverted') == (0x3f in world.owswaps[player][0] and world.owMixed[player]): set_rule(world.get_entrance('Octoballoon Mirror Spot', player), lambda state: state.has_Mirror(player)) else: set_rule(world.get_entrance('Bomber Corner Mirror Spot', player), lambda state: state.has_Mirror(player)) @@ -2050,7 +2050,7 @@ def set_inverted_big_bomb_rules(world, player): else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) - if world.owShuffle[player] != 'vanilla' or world.owSwap[player] != 'vanilla': + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player]: add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed diff --git a/asm/owrando.asm b/asm/owrando.asm index 8cd7bcf1..98be93c0 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -341,7 +341,7 @@ OWNewDestination: sep #$30 : lda OWOppSlotOffset,y : !add $04 : asl : and #$7f : sta $700 ; crossed OW shuffle - lda.l OWMode+1 : and #$ff : cmp #$02 : bne .return + lda.l OWMode+1 : and.b #$02 : beq .return ldx $05 : lda.l OWTileWorldAssoc,x : sta.l $7ef3ca ; change world ; toggle bunny mode diff --git a/data/base2current.bps b/data/base2current.bps index 6a4a34e265eac6877f69ae5e8469811901ebcee0..24256d1451f20acfbeb2557b8a911cb00c387721 100644 GIT binary patch delta 111 zcmV-#0FeLh&Is(z2(U>31cfhp(6dbesPzq(U6GXr&;ektZJSR$vjg`#4*><2KL!CF z0s`=tVFm#g5(*Kftw})1K+qATUC9^l7Na$niUt8113%y%x2FaH&QJjtmv&bHGyyrc Rt5*TM&j|HQVkzZ8KJ-`mDcJx3 delta 134 zcmV;10D1rH&Is?$2(U>31m7+o(X&kfsPzt$d%>9i1JD6ruWg$z4YLCGI}ZU3mp%po z9t8i%0??OU1_2i@4iToUNkGX!&=I9w$rtbzqd9n{oq!9aUC9^F1(OGWnWY7Vio!5p oqaRB2H{c?-qy_=bPyrK{b5{X00X?^ Date: Mon, 16 Aug 2021 22:47:43 -0500 Subject: [PATCH 053/104] Fixed issue with Sanc in TR start as bunny if TR tile is swapped --- OWEdges.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OWEdges.py b/OWEdges.py index b8c5ceab..4d231070 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -697,7 +697,6 @@ OWTileRegions = bidict({ 'Spectacle Rock Ledge': 0x03, 'West Death Mountain (Bottom)': 0x03, - 'Death Mountain Floating Island': 0x05, 'East Death Mountain (Top West)': 0x05, 'East Death Mountain (Top East)': 0x05, 'Spiral Cave Ledge': 0x05, @@ -706,6 +705,7 @@ OWTileRegions = bidict({ 'Fairy Ascension Plateau': 0x05, 'East Death Mountain (Bottom Left)': 0x05, 'East Death Mountain (Bottom)': 0x05, + 'Death Mountain Floating Island': 0x05, 'Death Mountain TR Pegs': 0x07, 'Death Mountain TR Pegs Ledge': 0x07, @@ -846,6 +846,9 @@ OWTileRegions = bidict({ 'East Dark Death Mountain (Top)': 0x45, 'East Dark Death Mountain (Bottom Left)': 0x45, 'East Dark Death Mountain (Bottom)': 0x45, + 'Dark Death Mountain Ledge': 0x45, + 'Dark Death Mountain Isolated Ledge': 0x45, + 'Dark Death Mountain Floating Island': 0x45, 'Turtle Rock Area': 0x47, 'Turtle Rock Ledge': 0x47, From 8ff463b5c745ae5e3f5bc765075f34fb5c66da01 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Mon, 16 Aug 2021 22:50:03 -0500 Subject: [PATCH 054/104] Plando support for OW edges --- OverworldShuffle.py | 12 ++- Plando.py | 175 ++++++++++++++++++++++++--------------- Plandomizer_Template.txt | 7 ++ 3 files changed, 123 insertions(+), 71 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 5d1d9669..b4da2cd5 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -164,8 +164,6 @@ def link_overworld(world, player): connected_edges = [] - connect_custom(world, connected_edges, player) - # layout shuffle if world.owShuffle[player] == 'vanilla' and not world.owCrossed[player]: # vanilla transitions @@ -208,6 +206,8 @@ def link_overworld(world, player): for exitname, destname in test_connections: connect_two_way(world, exitname, destname, player, connected_edges) + connect_custom(world, connected_edges, player) + trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) groups = reorganize_groups(world, trimmed_groups, player) @@ -321,7 +321,15 @@ def link_overworld(world, player): def connect_custom(world, connected_edges, player): if hasattr(world, 'custom_overworld') and world.custom_overworld[player]: for edgename1, edgename2 in world.custom_overworld[player]: + if edgename1 in connected_edges or edgename2 in connected_edges: + owedge1 = world.check_for_owedge(edgename1, player) + owedge2 = world.check_for_owedge(edgename2, player) + if owedge1.dest is not None and owedge1.dest.name == owedge2.name: + continue # if attempting to connect a pair that was already connected earlier, allow it to continue + raise RuntimeError('Invalid plando connection: rule violation based on current settings') connect_two_way(world, edgename1, edgename2, player, connected_edges) + if world.owKeepSimilar[player]: #TODO: If connecting an edge that belongs to a similar pair, the remaining edges need to get connected automatically + continue def connect_simple(world, exitname, regionname, player): world.get_entrance(exitname, player).connect(world.get_region(regionname, player)) diff --git a/Plando.py b/Plando.py index 2a4b911d..c1cd8c23 100755 --- a/Plando.py +++ b/Plando.py @@ -9,6 +9,7 @@ import sys from BaseClasses import World from Regions import create_regions +from OverworldShuffle import link_overworld from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name from Rules import set_rules @@ -42,6 +43,11 @@ def main(args): create_regions(world, 1) create_dungeons(world, 1) + text_patches = [] + prefill_world(world, args.plando, text_patches) + + link_overworld(world, 1) + link_entrances(world, 1) logger.info('Calculating Access Rules.') @@ -50,9 +56,7 @@ def main(args): logger.info('Fill the world.') - text_patches = [] - - fill_world(world, args.plando, text_patches) + fill_world(world, args.plando) if world.get_entrance('Dam', 1).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', 1).connected_region.name != 'Swamp Palace (Entrance)': world.swamp_patch_required[1] = True @@ -92,75 +96,14 @@ def main(args): return world -def fill_world(world, plando, text_patches): - mm_medallion = 'Ether' - tr_medallion = 'Quake' +def fill_world(world, plando): logger = logging.getLogger('') with open(plando, 'r') as plandofile: for line in plandofile.readlines(): + line = line.lstrip() if line.startswith('#'): continue - if ':' in line: - line = line.lstrip() - - if line.startswith('!'): - if line.startswith('!mm_medallion'): - _, medallionstr = line.split(':', 1) - mm_medallion = medallionstr.strip() - elif line.startswith('!tr_medallion'): - _, medallionstr = line.split(':', 1) - tr_medallion = medallionstr.strip() - elif line.startswith('!mode'): - _, modestr = line.split(':', 1) - world.mode = {1: modestr.strip()} - elif line.startswith('!logic'): - _, logicstr = line.split(':', 1) - world.logic = {1: logicstr.strip()} - elif line.startswith('!goal'): - _, goalstr = line.split(':', 1) - world.goal = {1: goalstr.strip()} - elif line.startswith('!light_cone_sewers'): - _, sewerstr = line.split(':', 1) - world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'} - elif line.startswith('!light_cone_lw'): - _, lwconestr = line.split(':', 1) - world.light_world_light_cone = lwconestr.strip().lower() == 'true' - elif line.startswith('!light_cone_dw'): - _, dwconestr = line.split(':', 1) - world.dark_world_light_cone = dwconestr.strip().lower() == 'true' - elif line.startswith('!fix_trock_doors'): - _, trdstr = line.split(':', 1) - world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'} - elif line.startswith('!fix_trock_exit'): - _, trfstr = line.split(':', 1) - world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'} - elif line.startswith('!fix_gtower_exit'): - _, gtfstr = line.split(':', 1) - world.fix_gtower_exit = gtfstr.strip().lower() == 'true' - elif line.startswith('!fix_pod_exit'): - _, podestr = line.split(':', 1) - world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'} - elif line.startswith('!fix_skullwoods_exit'): - _, swestr = line.split(':', 1) - world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'} - elif line.startswith('!check_beatable_only'): - _, chkbtstr = line.split(':', 1) - world.check_beatable_only = chkbtstr.strip().lower() == 'true' - elif line.startswith('!ganon_death_pyramid_respawn'): - _, gnpstr = line.split(':', 1) - world.ganon_at_pyramid = gnpstr.strip().lower() == 'true' - elif line.startswith('!save_quit_boss'): - _, sqbstr = line.split(':', 1) - world.save_and_quite_from_boss = sqbstr.strip().lower() == 'true' - elif line.startswith('!text_'): - textname, text = line.split(':', 1) - text_patches.append([textname.lstrip('!text_').strip(), 'text', text.strip()]) - #temporarilly removed. New credits system not ready to handle this. - #elif line.startswith('!credits_'): - # textname, text = line.split(':', 1) - # text_patches.append([textname.lstrip('!credits_').strip(), 'credits', text.strip()]) - continue - + elif ':' in line: locationstr, itemstr = line.split(':', 1) location = world.get_location(locationstr.strip(), 1) if location is None: @@ -172,6 +115,7 @@ def fill_world(world, plando, text_patches): world.push_item(location, item) if item.smallkey or item.bigkey: location.event = True + #TODO: move entrance stuff to prefill_world to work like OW stuff elif '<=>' in line: entrance, exit = line.split('<=>', 1) connect_two_way(world, entrance.strip(), exit.strip(), 1) @@ -182,14 +126,107 @@ def fill_world(world, plando, text_patches): entrance, exit = line.split('<=', 1) connect_exit(world, exit.strip(), entrance.strip(), 1) - world.required_medallions[1] = (mm_medallion, tr_medallion) - # set up Agahnim Events world.get_location('Agahnim 1', 1).event = True world.get_location('Agahnim 1', 1).item = ItemFactory('Beat Agahnim 1', 1) world.get_location('Agahnim 2', 1).event = True world.get_location('Agahnim 2', 1).item = ItemFactory('Beat Agahnim 2', 1) +def prefill_world(world, plando, text_patches): + mm_medallion = 'Ether' + tr_medallion = 'Quake' + logger = logging.getLogger('') + with open(plando, 'r') as plandofile: + for line in plandofile.readlines(): + line = line.lstrip() + if line.startswith('#'): + continue + elif line.startswith('!'): + if line.startswith('!mm_medallion'): + _, medallionstr = line.split(':', 1) + mm_medallion = medallionstr.strip() + elif line.startswith('!tr_medallion'): + _, medallionstr = line.split(':', 1) + tr_medallion = medallionstr.strip() + elif line.startswith('!mode'): + _, modestr = line.split(':', 1) + world.mode = {1: modestr.strip()} + elif line.startswith('!logic'): + _, logicstr = line.split(':', 1) + world.logic = {1: logicstr.strip()} + elif line.startswith('!goal'): + _, goalstr = line.split(':', 1) + world.goal = {1: goalstr.strip()} + elif line.startswith('!owShuffle'): + _, modestr = line.split(':', 1) + world.owShuffle = {1: modestr.strip()} + elif line.startswith('!owCrossed'): + _, modestr = line.split(':', 1) + modestr = modestr.strip().lower() + world.owCrossed = {1: True if modestr in ('true', 'yes', 'on', 'enabled') else False} + elif line.startswith('!owKeepSimilar'): + _, modestr = line.split(':', 1) + modestr = modestr.strip().lower() + world.owKeepSimilar = {1: True if modestr in ('true', 'yes', 'on', 'enabled') else False} + elif line.startswith('!light_cone_sewers'): + _, sewerstr = line.split(':', 1) + world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'} + elif line.startswith('!light_cone_lw'): + _, lwconestr = line.split(':', 1) + world.light_world_light_cone = lwconestr.strip().lower() == 'true' + elif line.startswith('!light_cone_dw'): + _, dwconestr = line.split(':', 1) + world.dark_world_light_cone = dwconestr.strip().lower() == 'true' + elif line.startswith('!fix_trock_doors'): + _, trdstr = line.split(':', 1) + world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'} + elif line.startswith('!fix_trock_exit'): + _, trfstr = line.split(':', 1) + world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'} + elif line.startswith('!fix_gtower_exit'): + _, gtfstr = line.split(':', 1) + world.fix_gtower_exit = gtfstr.strip().lower() == 'true' + elif line.startswith('!fix_pod_exit'): + _, podestr = line.split(':', 1) + world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'} + elif line.startswith('!fix_skullwoods_exit'): + _, swestr = line.split(':', 1) + world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'} + elif line.startswith('!check_beatable_only'): + _, chkbtstr = line.split(':', 1) + world.check_beatable_only = chkbtstr.strip().lower() == 'true' + elif line.startswith('!ganon_death_pyramid_respawn'): + _, gnpstr = line.split(':', 1) + world.ganon_at_pyramid = gnpstr.strip().lower() == 'true' + elif line.startswith('!save_quit_boss'): + _, sqbstr = line.split(':', 1) + world.save_and_quite_from_boss = sqbstr.strip().lower() == 'true' + elif line.startswith('!text_'): + textname, text = line.split(':', 1) + text_patches.append([textname.lstrip('!text_').strip(), 'text', text.strip()]) + #temporarilly removed. New credits system not ready to handle this. + #elif line.startswith('!credits_'): + # textname, text = line.split(':', 1) + # text_patches.append([textname.lstrip('!credits_').strip(), 'credits', text.strip()]) + continue + elif line.startswith('$'): + edge1, edge2 = line.split('=', 1) + if world.custom_overworld is None: + world.custom_overworld = {1: []} + world.custom_overworld[1].append(edge1.strip(), edge2.strip()) + #TODO: Do entrances similar to OW + elif '<=>' in line: + entrance, exit = line.split('<=>', 1) + #connect_two_way(world, entrance.strip(), exit.strip(), 1) + elif '=>' in line: + entrance, exit = line.split('=>', 1) + #connect_entrance(world, entrance.strip(), exit.strip(), 1) + elif '<=' in line: + entrance, exit = line.split('<=', 1) + #connect_exit(world, exit.strip(), entrance.strip(), 1) + + world.required_medallions[1] = (mm_medallion, tr_medallion) + def start(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/Plandomizer_Template.txt b/Plandomizer_Template.txt index 6c94cc76..dce3f005 100644 --- a/Plandomizer_Template.txt +++ b/Plandomizer_Template.txt @@ -242,3 +242,10 @@ Ganons Tower - Mini Helmasaur Room - Right: Nothing Ganons Tower - Pre-Moldorm Chest: Small Key (Ganons Tower) Ganons Tower - Validation Chest: Nothing Ganon: Triforce + +# set Overworld connections (lines starting with $, separate edges with =) +!owShuffle: parallel +#!owMixed: true # Mixed OW not supported yet +!owCrossed: true +!owKeepSimilar: true +$Links House NE = Kakariko Village SE From 5fb394aba1bef9e931cb0320f1e55bab2e1160dc Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 17 Aug 2021 10:26:05 -0500 Subject: [PATCH 055/104] Fixed issue with Pyramid Hole not shuffling --- EntranceShuffle.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 0863010b..6d3e0099 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1029,16 +1029,6 @@ def scramble_holes(world, player): ('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'), ('Lumberjack Tree Exit', 'Lumberjack Tree (top)')] - if not world.shuffle_ganon: - if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): - connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) - else: - connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) - connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) - else: - hole_targets.append(('Pyramid Exit', 'Pyramid')) - if world.mode[player] == 'standard': # cannot move uncle cave connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player) @@ -1050,18 +1040,31 @@ def scramble_holes(world, player): # do not shuffle sanctuary into pyramid hole unless shuffle is crossed if world.shuffle[player] == 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) - if world.shuffle_ganon: - random.shuffle(hole_targets) - exit, target = hole_targets.pop() + + # determine pyramid hole + if not world.shuffle_ganon: if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player) connect_entrance(world, 'Pyramid Hole', 'Pyramid', player) else: connect_two_way(world, 'Inverted Pyramid Entrance', 'Pyramid Exit', player) connect_entrance(world, 'Inverted Pyramid Hole', 'Pyramid', player) + else: + hole_targets.append(('Pyramid Exit', 'Pyramid')) + + random.shuffle(hole_targets) + exit, target = hole_targets.pop() + if (world.mode[player] == 'inverted') == (0x03 in world.owswaps[player][0] and world.owMixed[player]): + connect_two_way(world, 'Pyramid Entrance', exit, player) + connect_entrance(world, 'Pyramid Hole', target, player) + else: + connect_two_way(world, 'Inverted Pyramid Entrance', exit, player) + connect_entrance(world, 'Inverted Pyramid Hole', target, player) + if world.shuffle[player] != 'crossed': hole_targets.append(('Sanctuary Exit', 'Sewer Drop')) + # shuffle the rest random.shuffle(hole_targets) for entrance, drop in hole_entrances: exit, target = hole_targets.pop() From ec327ca5a9adb30176523cd688b136c5cd27dec8 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 17 Aug 2021 15:07:50 -0500 Subject: [PATCH 056/104] Version bump 0.1.8.0 --- CHANGELOG.md | 8 ++++++++ OverworldShuffle.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e97665ce..2a113cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 0.1.8.0 +- Moved Crossed to its own checkbox option +- Removed Legacy ER shuffles +- Added OW Shuffle support for Plando module (needs user testing) +- Fixed issue with Sanc start at TR as bunny when it is LW +- Fixed issue with Pyramid Hole not getting shuffled +- ~~Merged DR v0.5.0.3 - Minor DR fixes~~ + ### 0.1.7.4 - Fixed issue with Mixed OW failing to generate when HC/Pyramid is swapped - Various fixes to improve generation rates for Mixed OW Shuffle diff --git a/OverworldShuffle.py b/OverworldShuffle.py index b4da2cd5..c41f9097 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.7.4-u' +__version__ = '0.1.8.0-u' def link_overworld(world, player): # setup mandatory connections From 621e6442ce3e3a7c8cf454fd4651db9a431fae3c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 18 Aug 2021 09:23:13 -0500 Subject: [PATCH 057/104] Fixed Standard mode to exclude Flute exits from traversal logic --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 769f4c4a..7c6250c0 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1868,7 +1868,7 @@ def find_accessible_entrances(world, player, builder): connect = ext.connected_region if connect is None or ext.door and ext.door.blocked: continue - if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' and ext.name in ['Hyrule Castle Main Gate (North)', 'Top of Pyramid (Inner)', 'Inverted Pyramid Entrance' ]: + if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' and (ext.name.startswith('Flute From') or ext.name in ['Hyrule Castle Main Gate (North)', 'Top of Pyramid (Inner)', 'Inverted Pyramid Entrance']): continue if connect.name in entrances and connect not in visited_entrances: visited_entrances.append(connect.name) From e06b99854b9a69d865488d71bf233b5fcc83dde8 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 08:36:25 -0500 Subject: [PATCH 058/104] Fixed missing Swordless Mire entry rule --- Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Rules.py b/Rules.py index 3e50c6d4..5b40ec28 100644 --- a/Rules.py +++ b/Rules.py @@ -1487,6 +1487,7 @@ def swordless_rules(world, player): if world.mode[player] != 'inverted': set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle + set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock Ledge', 'Region', player)) # sword not required to use medallion for opening in swordless (!) add_bunny_rule(world.get_entrance('Turtle Rock', player), player) add_bunny_rule(world.get_entrance('Misery Mire', player), player) From f2432e13fb2adbd6c51d8bb700bbf138506b62b7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 08:37:46 -0500 Subject: [PATCH 059/104] Added new Dark Graveyard North region --- OWEdges.py | 1 + OverworldShuffle.py | 2 ++ Regions.py | 3 ++- Rules.py | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/OWEdges.py b/OWEdges.py index 4d231070..ebf1b15b 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -870,6 +870,7 @@ OWTileRegions = bidict({ 'Dark Chapel Area': 0x53, 'Dark Graveyard Area': 0x54, + 'Dark Graveyard North': 0x54, 'Qirn Jump Area': 0x55, 'Qirn Jump East Bank': 0x55, diff --git a/OverworldShuffle.py b/OverworldShuffle.py index c41f9097..ba2e4152 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -964,6 +964,8 @@ mandatory_connections = [# Whirlpool Connections ('Skull Woods Pass Bush Row (East)', 'Skull Woods Pass West Area'), #pearl ('Skull Woods Pass Rock (Top)', 'Skull Woods Pass East Bottom Area'), #mitts ('Skull Woods Pass Rock (Bottom)', 'Skull Woods Pass East Top Area'), #mitts + ('Dark Graveyard Bush (South)', 'Dark Graveyard North'), #pearl + ('Dark Graveyard Bush (North)', 'Dark Graveyard Area'), #pearl ('Qirn Jump Water Drop', 'Qirn Jump Water'), #flippers ('Qirn Jump East Water Drop', 'Qirn Jump Water'), #flippers ('Qirn Jump Pier', 'Qirn Jump East Bank'), diff --git a/Regions.py b/Regions.py index 9ce0abbc..a7907180 100644 --- a/Regions.py +++ b/Regions.py @@ -148,7 +148,8 @@ def create_regions(world, player): create_dw_region(player, 'Dark Fortune Area', None, ['Fortune Teller (Dark)', 'Kakariko Fortune Mirror Spot', 'Dark Fortune NE', 'Dark Fortune EN', 'Dark Fortune ES', 'Dark Fortune SC']), create_dw_region(player, 'Outcast Pond Area', None, ['Kakariko Pond Mirror Spot', 'Outcast Pond NE', 'Outcast Pond WN', 'Outcast Pond WS', 'Outcast Pond SW', 'Outcast Pond SE', 'Outcast Pond EN', 'Outcast Pond ES']), create_dw_region(player, 'Dark Chapel Area', None, ['Dark Sanctuary Hint', 'Sanctuary Mirror Spot', 'Bonk Rock Ledge Mirror Spot', 'Dark Chapel WN', 'Dark Chapel WS', 'Dark Chapel EC']), - create_dw_region(player, 'Dark Graveyard Area', None, ['Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Dark Graveyard WC', 'Dark Graveyard EC']), + create_dw_region(player, 'Dark Graveyard Area', None, ['Dark Graveyard Bush (South)', 'Dark Graveyard WC', 'Dark Graveyard EC']), + create_dw_region(player, 'Dark Graveyard North', None, ['Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Dark Graveyard Bush (North)']), create_dw_region(player, 'Qirn Jump Area', None, ['Qirn Jump Water Drop', 'River Bend Mirror Spot', 'Qirn Jump WC', 'Qirn Jump SW']), create_dw_region(player, 'Qirn Jump East Bank', None, ['Qirn Jump East Water Drop', 'River Bend East Mirror Spot', 'Qirn Jump SE', 'Qirn Jump EC', 'Qirn Jump ES']), create_dw_region(player, 'Qirn Jump Water', None, ['Qirn Jump Pier', 'Qirn Jump Whirlpool', 'Qirn Jump EN', 'Qirn Jump SC'], Terrain.Water), diff --git a/Rules.py b/Rules.py index 5b40ec28..a148be20 100644 --- a/Rules.py +++ b/Rules.py @@ -1263,6 +1263,8 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (East)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Top)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Rock (Bottom)', player), player) + add_bunny_rule(world.get_entrance('Dark Graveyard Bush (South)', player), player) + add_bunny_rule(world.get_entrance('Dark Graveyard Bush (North)', player), player) add_bunny_rule(world.get_entrance('Dark Witch Rock (North)', player), player) add_bunny_rule(world.get_entrance('Dark Witch Rock (South)', player), player) add_bunny_rule(world.get_entrance('Catfish Approach Rocks (West)', player), player) From 340e4f766e6c6eab3578dcfe875a899a2494aba0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 08:38:52 -0500 Subject: [PATCH 060/104] Added new indirect connections in prep for Big Bomb rules --- EntranceShuffle.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 6d3e0099..7e3ec63a 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2369,13 +2369,24 @@ inverted_default_dungeon_connections = [('Ganons Tower', 'Agahnims Tower Portal' indirect_connections = { 'Turtle Rock Ledge': 'Turtle Rock', - 'Pyramid Area': 'Pyramid Fairy', - #'East Dark World': 'Pyramid Fairy', 'Big Bomb Shop': 'Pyramid Fairy', + #'East Dark World': 'Pyramid Fairy', + 'Pyramid Area': 'Pyramid Fairy', # HC Ledge/Courtyard #'Dark Desert': 'Pyramid Fairy', + 'Misery Mire Area': 'Pyramid Fairy', # Desert/Checkerboard Ledge #'West Dark World': 'Pyramid Fairy', + 'Dark Chapel Area': 'Pyramid Fairy', # Bonk Rocks + 'Dark Graveyard North': 'Pyramid Fairy', # Graveyard Ledge/Kings Tomb #'South Dark World': 'Pyramid Fairy', + 'Dig Game Ledge': 'Pyramid Fairy', # Brother House Left + 'Stumpy Approach Area': 'Pyramid Fairy', # Cave 45 + # Inverted Cases #'Light World': 'Pyramid Fairy', + 'Lost Woods West Area': 'Pyramid Fairy', # Skull Woods Back + 'East Death Mountain (Top East)': 'Pyramid Fairy', # Floating Island + 'Blacksmith Area': 'Pyramid Fairy', # Hammerpegs + 'Forgotten Forest Area': 'Pyramid Fairy', # Shield Shop + 'Desert Area': 'Pyramid Fairy', # Mire Area 'Old Man Cave': 'Old Man S&Q' } # format: From 03e9f24ce2b7519cec51da412264ca9420c6f785 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 09:58:41 -0500 Subject: [PATCH 061/104] Fixed infinite loop issue in OW Shuffle --- OverworldShuffle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index ba2e4152..6b3aca9d 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -230,11 +230,15 @@ def link_overworld(world, player): f += 1 if f < len(forward_edge_sets): forward_set = forward_edge_sets[f] + else: + break f += 1 while back_set[0] in connected_edges: b += 1 if b < len(back_edge_sets): back_set = back_edge_sets[b] + else: + break b += 1 assert len(forward_set) == len(back_set) for (forward_edge, back_edge) in zip(forward_set, back_set): From 25bc968b6281500fdaaaa6d1bf401631ee7807e1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Aug 2021 16:15:05 -0600 Subject: [PATCH 062/104] Key counts for vanilla fixed --- Dungeons.py | 7 ------- KeyDoorShuffle.py | 5 +++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Dungeons.py b/Dungeons.py index 75699434..73f53794 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,13 +375,6 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } -default_key_counts = { - 'Hyrule Castle': 1, 'Eastern Palace': 0, 'Desert Palace': 1, - 'Tower of Hera': 1, 'Agahnims Tower': 2, 'Palace of Darkness': 6, - 'Swamp Palace': 1, 'Skull Woods': 3, 'Thieves Town': 1, - 'Ice Palace': 2, 'Misery Mire': 3, 'Turtle Rock': 4, 'Ganons Tower': 4 -} - dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 4319d42b..5cf88dc0 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,7 +4,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize, default_key_counts +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize from DungeonGenerator import ExplorationState, special_big_key_doors @@ -1531,7 +1531,8 @@ def create_key_counters(key_layout, world, player): flat_proposal = key_layout.flat_prop state = ExplorationState(dungeon=key_layout.sector.name) if world.doorShuffle[player] == 'vanilla': - state.key_locations = default_key_counts[key_layout.sector.name] + builder = world.dungeon_layouts[player][key_layout.sector.name] + state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt else: builder = world.dungeon_layouts[player][key_layout.sector.name] state.key_locations = builder.total_keys - builder.key_drop_cnt From 4e91e627e1888ed24683755c72e6053d32709fe9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 19 Aug 2021 16:21:42 -0600 Subject: [PATCH 063/104] Split dungeon refinement Prize relevance refinement --- DungeonGenerator.py | 30 +++++++++++++++++++++++++++++- KeyDoorShuffle.py | 9 +++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 170233ce..948115f3 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -3320,6 +3320,7 @@ def check_for_valid_layout(builder, sector_list, builder_info): possible_regions.add(portal.door.entrance.parent_region.name) if builder.name in dungeon_drops.keys(): possible_regions.update(dungeon_drops[builder.name]) + independents = find_independent_entrances(possible_regions, world, player) for name, split_build in builder.split_dungeon_map.items(): name_bits = name.split(" ") orig_name = " ".join(name_bits[:-1]) @@ -3332,7 +3333,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): if r_name not in entrance_regions: entrance_regions.append(r_name) # entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid] - proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player) + split = any(x for x in independents if x not in entrance_regions) + proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player) # record split proposals builder.valid_proposal[name] = proposal builder.exception_list = list(sector_list) @@ -3347,6 +3349,32 @@ def check_for_valid_layout(builder, sector_list, builder_info): return len(unreached_doors) == 0, unreached_doors +def find_independent_entrances(entrance_regions, world, player): + independents = set() + for region in entrance_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None) + if portal: + if portal.destination: + continue + elif len(entrance_regions) > 1: + p_region = portal.door.entrance.connected_region + access_region = next(x.parent_region for x in p_region.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) + if access_region.name in world.inaccessible_regions[player]: + continue + else: + r = world.get_region(region, player) + access_region = next(x.parent_region for x in r.entrances + if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] + or x.parent_region.name == 'Sewer Drop') + if access_region.name == 'Sewer Drop': + access_region = next(x.parent_region for x in access_region.entrances) + if access_region.name in world.inaccessible_regions[player]: + continue + independents.add(region) + return independents + + def resolve_equations(builder, sector_list): unreached_doors = defaultdict(list) equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0} diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5cf88dc0..cf18f4f4 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1385,10 +1385,12 @@ def validate_key_layout(key_layout, world, player): state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if (key_layout.prize_relevant and dungeon_entrance and - dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy']): + if (len(key_layout.start_regions) > 1 and dungeon_entrance and + dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] + and key_layout.key_logic.dungeon in dungeon_prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance + key_layout.prize_relevant = True else: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) @@ -1419,6 +1421,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if smalls_done and bk_done: return False else: + # todo: pretty sure you should OR these paths together, maybe when there's one location and it can + # either be small or big key if smalls_avail and available_small_locations > 0: for exp_door in state.small_doors: state_copy = state.copy() @@ -1449,6 +1453,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa valid = checked_states[code] if not valid: return False + # todo: feel like you only open these if the boss is available??? if not state.prize_doors_opened and key_layout.prize_relevant: state_copy = state.copy() open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) From 4e8758c647c1dcae145cd0c4e7e0d067d639c36b Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 18:48:44 -0500 Subject: [PATCH 064/104] Fixed missing OW flag check changes --- Rom.py | 2 +- data/base2current.bps | Bin 141164 -> 141164 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 26911e9f..2214c15b 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '99f3f57ab2c9449172cade4927a462d6' +RANDOMIZERBASEHASH = '0a8d3886c789945bd64c4c3f16a6194f' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 24256d1451f20acfbeb2557b8a911cb00c387721..fa7152f57bb8c20c79c0c3e79787fa7d1dca50e6 100644 GIT binary patch delta 38 ucmaEJj^oWajt!oS%=P*Wn|&CC3K?0NW)N@eNt`aJ+D6%T>{ delta 38 ucmaEJj^oWajt!oS%&q!Wn|&CC3K Date: Thu, 19 Aug 2021 20:02:12 -0500 Subject: [PATCH 065/104] Moved OW flag checks to global variable --- Rom.py | 2 +- asm/owrando.asm | 2 +- data/base2current.bps | Bin 141164 -> 141164 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 2214c15b..eb8b6e6e 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '0a8d3886c789945bd64c4c3f16a6194f' +RANDOMIZERBASEHASH = '3f7cd4772ea4a3f503691ef8807f5d84' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 98be93c0..63107a0e 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -341,7 +341,7 @@ OWNewDestination: sep #$30 : lda OWOppSlotOffset,y : !add $04 : asl : and #$7f : sta $700 ; crossed OW shuffle - lda.l OWMode+1 : and.b #$02 : beq .return + LDA.l OWMode+1 : AND.b #!FLAG_OW_CROSSED : beq .return ldx $05 : lda.l OWTileWorldAssoc,x : sta.l $7ef3ca ; change world ; toggle bunny mode diff --git a/data/base2current.bps b/data/base2current.bps index fa7152f57bb8c20c79c0c3e79787fa7d1dca50e6..08be9f59afac55702b9c3e60f5a4916cab14174c 100644 GIT binary patch delta 39 xcmV+?0NDTR&Is(z2(U>31ivsq&$CSd5OD!1gI;pCUUC5jwFuxu&hR#(_Y_L;5Rd=> delta 39 xcmV+?0NDTR&Is(z2(U>31b;7p(6dbe5OD#?gI;pCUUC5jwFo}g>LjJ7f4ozA5f%Ud From b58a194956b0d063a2a5eb3984d3f5a43016ea6c Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 20:34:02 -0500 Subject: [PATCH 066/104] Fixed Parallel+Crossed failing generation --- OverworldShuffle.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 6b3aca9d..9a058cb6 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -231,6 +231,7 @@ def link_overworld(world, player): if f < len(forward_edge_sets): forward_set = forward_edge_sets[f] else: + forward_set = None break f += 1 while back_set[0] in connected_edges: @@ -238,11 +239,17 @@ def link_overworld(world, player): if b < len(back_edge_sets): back_set = back_edge_sets[b] else: + back_set = None break b += 1 - assert len(forward_set) == len(back_set) - for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player, connected_edges) + if forward_set is not None and back_set is not None: + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): + connect_two_way(world, forward_edge, back_edge, player, connected_edges) + elif forward_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) + elif back_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) else: # vanilla/crossed shuffle for (forward_edge_sets, back_edge_sets) in groups: From af1960fe663471282368d2241136adf7c5307325 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 20:37:55 -0500 Subject: [PATCH 067/104] Version bump 0.1.8.1 --- CHANGELOG.md | 7 +++++++ OverworldShuffle.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a113cdc..8b287ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 0.1.8.1 +- Fixed issue with activating flute in DW (OW Mixed) +- Fixed issue with Parallel+Crossed not generating +- Fixed issue with Standard not generating +- Fixed issue with Swordless not generating +- Fixed logic for Graveyard Ledge and Kings Tomb + ### 0.1.8.0 - Moved Crossed to its own checkbox option - Removed Legacy ER shuffles diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 9a058cb6..0eb7f78e 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.8.0-u' +__version__ = '0.1.8.1-u' def link_overworld(world, player): # setup mandatory connections From ee244f2b24f8d7e0fe826e3161f853c37b0457dd Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 19 Aug 2021 21:53:45 -0500 Subject: [PATCH 068/104] Actually fixed Mixed flag problems --- Rom.py | 2 +- data/base2current.bps | Bin 141164 -> 141165 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index eb8b6e6e..1bcc30d0 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '3f7cd4772ea4a3f503691ef8807f5d84' +RANDOMIZERBASEHASH = '6d8c7d1f0530c1ed1da5afa60e7a6d98' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 08be9f59afac55702b9c3e60f5a4916cab14174c..7246d99df4b6f3c211066d371982a23d9bbc0803 100644 GIT binary patch delta 1658 zcmWkseNlt4l1lqp|5g_#irE8j@Nd(p@O zi4ksNj){V#U;*rg3490M2QjZC1A!zK_!0pHunWtDPvA$~M?I6J-s8U0v-4ae zJCUcmu2*7}Xm{w3^IVd)kIo(77RgFqqRvR)z)4XWtiff`9Z=&Z0Zoaiv)p0UNGoT# zaU9VdtpHFt$NH25`KJKA^dyn&FclOW*JS1iUiXL%{ke zJ#GD$CF;dfdY^wwZBJ>>x14W1`_IysFe7vw%<@&C{RzR;JXaanrGM@g7uKm?Kw0{5 zuz%X@A^qnbjxhKS#@|d|1MnrzZOejcq%*wm4K8KugeX2W)4Cb1^LOjC}rmQNZ!v*E($&5xJ2a&;-GZR9HmNSw&JzsJlKPQEy-{Ivs%ifvIlApNqCiV z84k6mAP;4&;lWhulQG5cRJQi#sYRIF`eyXuB;)1UX^o2@(*9F3tuY<8UFz;Jn~qLv zri!r;-)n7!O}MEo8Zxnx|LcOALB#_=|HNDEmmA3gf zSFj1z6D}bhz`wTbgi9EHd{aSO;ZK(pQop5r4JSZV1(uv6$OTzszjif%N*_B@_BzM6^-{kv`+M~>%j z*|D^i*=)2DYS%HHRqY_$9cq`tUOWVc#%b210x&XkySR~`-byU$}u!Lnn}y{ll^AF%f8=H#~0yk%X;)!q{$ zZ`ZKqN+&TyOz~T4S=~&MEsN0!zqbu`Z(%Hl9H};avZS%4Vcc*bgk$~ijHLL15f-7+l7ibu9>+G&%^@lVG;5>T0i273_y!F%V0fk7=ykIWI^R(dfd zbI6j+XG~UuIJ@?i&KqKDL)XWDh7`VON&(QyPhCqO!@?ttR|8!DrbJp}DyB8=7c52m ze`kWk5QC!Ic~HaexqU$pT9I$;qfdQ2I45`4;V*P7Z4cwA&r;$^q75^aS|Of)e<@Ey ziRxI3lBZTmB#>k&7r+jPpWp=WG6`>3ycE!&=v<;;Vu>Q(iWMKZ*LD5P&%*EjANwu5 AA^-pY delta 1635 zcmWkse^65g63)K7ge1@q0U>Y*;mM(8P!SP4en`}6V*p|BCZ&piSC~@c!JL2rL3l43 zX-r~-m8K9Nk6)z$Y@n}bdmTjRSsUwF>Uc9Z#ha;kayBPUETC7Msh94|&VK*w?)U9} zi+4m%?uf?r138tb{0?(rA6&spa0UFhY{DOiS5LS?z5w72EGA#TOVkK{m8INgpBU){ zR?b{i8~$cgW0`PY@XG}@QQu9y+0S~3Qcr@xLVb;s!W|&T=fXR13~%_IOh}nyk24lZ zJ;#n&s2$Szo@Mbp4?D^+-(^(b3fDHFacqW$uou{23Nz^9@TxQz$I z1u%-e;uzVAZ+WRHd*r6XSm>}&<#<<|CcZVtdh+R3iic|@>qQs5Yy#6x-9x2hlhikd zidA!^oWf$Vcy}k9lpNlf3c-LvA&W|`wE^9+7932^`PRYy+5&} z!$@`f#}*NJ&gk)})VCI0{E78!WF8d7V@B`>_<<`6?unC<3v8+UyfN?=8`5HYg0i$> zsgJF4$av4q^31-2ao?nc1Kh{?^eiYtDq|KN;!?&Q2=L^8 z^ziGX7muFUF4$N2Z(N7=6Ne;~b8hk?q-hnE`07NpK=Vf$_oA-)53n1@t2J;KMKw{7 zj2mh)eeW-?+2J4UW@4=w^x*K+HT<}S3QPWm3F*|x&M^VlhFW*)nCR5%2p-1=HSdMw zwa3tk8QDGFAfa*{V-jciIUQJB8ylf60J`1vL4I>un$^nF+xiwW=_TF7n7G0jz(P^@yr3eLPR41S8uBopZX3Lf%DQ4n;A5?u;LT<$ z0`cRz3Q*yi`n6Ji>Wj;&;VGs5H`)SBu7Ag8)(`6=Ar~j=>mdQRoQi^UJaFo*P4+{n zx7z%+ow+PJ+%jhFtlNFZcN^* zah1duJ08Lrs3-&5?mr{Lw{6&kjLNQxdDU9RmzNV1g%%v*8|B7iw~!_YJuA1ctKPA0#rmh^r?tXQv++{Xbb$C_ThM$$tW|U!Y|v&3MXhQY%$fQL3e($&#BPI z)7lPmn$Z#v1QpQzO8o2PbK>=f%@-mvX-5jT|C2gGxVddFO|9U~v8}-8ei{3cFUZO> zJEYtfpLLP`k~+Fo<|b$T`}thy7kByo`)kaMDgYgK{dyb`5+=7y_&Wit2~?A{fNFC6&Q`#!&IE`c2K~NO!*Q_3jTwdWhq$E~$kAvWl_o=Y*?;V%Tge;lnP7yV}c# hEd;z{`(6MPs9Tl=aCKQg{3S;8;~RIpDSa Date: Fri, 20 Aug 2021 05:00:06 -0500 Subject: [PATCH 069/104] Fix for keeping Std connections vanilla in Standard+Parallel OW --- OverworldShuffle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 0eb7f78e..c9cc6c24 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -215,7 +215,11 @@ def link_overworld(world, player): # all layout shuffling occurs here if world.owShuffle[player] != 'vanilla': # layout shuffle - random.shuffle(groups) + if world.mode[player] == 'standard': + random.shuffle(groups[2:]) # keep first 2 groups (Standard) first + else: + random.shuffle(groups) + for (forward_edge_sets, back_edge_sets) in groups: assert len(forward_edge_sets) == len(back_edge_sets) random.shuffle(forward_edge_sets) From ed1332b691505869e56208cd9f497e83c9a0e640 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 05:47:15 -0500 Subject: [PATCH 070/104] Combined grouping combinations of Parallel+NonCrossed and Vanilla+Crossed --- OverworldShuffle.py | 266 +++++++++++++++----------------------------- 1 file changed, 92 insertions(+), 174 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index c9cc6c24..bf3217e2 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -590,234 +590,152 @@ def reorganize_groups(world, groups, player): new_grouping[(region, axis, terrain)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) - elif world.owShuffle[player] == 'parallel': - if world.owCrossed[player]: - if world.owKeepSimilar[player]: - if world.mode[player] == 'standard': - # tuple goes to (A,_,C,D,E,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, _, axis, terrain, parallel, count) = group - new_grouping[(std, axis, terrain, parallel, count)] = ([], []) - - for group in grouping.keys(): - (std, _, axis, terrain, parallel, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,_,C,D,E,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, _, axis, terrain, parallel, count) = group - new_grouping[(axis, terrain, parallel, count)] = ([], []) - - for group in grouping.keys(): - (_, _, axis, terrain, parallel, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.mode[player] == 'standard': - # tuple goes to (A,_,C,D,E,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, _, axis, terrain, parallel, _) = group - new_grouping[(std, axis, terrain, parallel)] = ([], []) - - for group in grouping.keys(): - (std, _, axis, terrain, parallel, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,_,C,D,E,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, _, axis, terrain, parallel, _) = group - new_grouping[(axis, terrain, parallel)] = ([], []) - - for group in grouping.keys(): - (_, _, axis, terrain, parallel, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.owKeepSimilar[player]: - if world.mode[player] == 'standard': - # tuple stays (A,B,C,D,E,F) - for grouping in (groups,): - return list(grouping.values()) - else: - # tuple goes to (_,B,C,D,E,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, count) = group - new_grouping[(region, axis, terrain, parallel, count)] = ([], []) - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.mode[player] == 'standard': - # tuple goes to (A,B,C,D,E,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, region, axis, terrain, parallel, _) = group - new_grouping[(std, region, axis, terrain, parallel)] = ([], []) - - for group in grouping.keys(): - (std, region, axis, terrain, parallel, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, parallel)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,B,C,D,E,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, _) = group - new_grouping[(region, axis, terrain, parallel)] = ([], []) - - for group in grouping.keys(): - (_, region, axis, terrain, parallel, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - elif world.owShuffle[player] == 'vanilla' and world.owCrossed[player]: + elif world.owShuffle[player] == 'parallel' and world.owCrossed[player]: if world.owKeepSimilar[player]: if world.mode[player] == 'standard': - # tuple goes to (A,B,C,D,_,F) + # tuple goes to (A,_,C,D,E,F) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (std, region, axis, terrain, _, count) = group - new_grouping[(std, region, axis, terrain, count)] = ([], []) + (std, _, axis, terrain, parallel, count) = group + new_grouping[(std, axis, terrain, parallel, count)] = ([], []) for group in grouping.keys(): - (std, region, axis, terrain, _, count) = group + (std, _, axis, terrain, parallel, count) = group (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, count)] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel, count)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(std, region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) + new_grouping[(std, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: - # tuple goes to (_,B,C,D,_,F) + # tuple goes to (_,_,C,D,E,F) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (_, region, axis, terrain, _, count) = group - new_grouping[(region, axis, terrain, count)] = ([], []) + (_, _, axis, terrain, parallel, count) = group + new_grouping[(axis, terrain, parallel, count)] = ([], []) for group in grouping.keys(): - (_, region, axis, terrain, _, count) = group + (_, _, axis, terrain, parallel, count) = group (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, count)] + (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel, count)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) + new_grouping[(axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: if world.mode[player] == 'standard': - # tuple goes to (A,B,C,D,_,_) + # tuple goes to (A,_,C,D,E,_) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (std, region, axis, terrain, _, _) = group - new_grouping[(std, region, axis, terrain)] = ([], []) + (std, _, axis, terrain, parallel, _) = group + new_grouping[(std, axis, terrain, parallel)] = ([], []) for group in grouping.keys(): - (std, region, axis, terrain, _, _) = group + (std, _, axis, terrain, parallel, _) = group (forward_edges, back_edges) = grouping[group] forward_edges = [[i] for l in forward_edges for i in l] back_edges = [[i] for l in back_edges for i in l] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain)] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(std, region, axis, terrain)] = (exist_forward_edges, exist_back_edges) + new_grouping[(std, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: - # tuple goes to (_,B,C,D,_,_) + # tuple goes to (_,_,C,D,E,_) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (_, region, axis, terrain, _, _) = group - new_grouping[(region, axis, terrain)] = ([], []) + (_, _, axis, terrain, parallel, _) = group + new_grouping[(axis, terrain, parallel)] = ([], []) for group in grouping.keys(): - (_, region, axis, terrain, _, _) = group + (_, _, axis, terrain, parallel, _) = group (forward_edges, back_edges) = grouping[group] forward_edges = [[i] for l in forward_edges for i in l] back_edges = [[i] for l in back_edges for i in l] - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain)] + (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain)] = (exist_forward_edges, exist_back_edges) + new_grouping[(axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + elif world.owShuffle[player] == 'parallel' or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player]): + if world.owKeepSimilar[player]: + if world.mode[player] == 'standard': + # tuple stays (A,B,C,D,E,F) + for grouping in (groups,): + return list(grouping.values()) + else: + # tuple goes to (_,B,C,D,E,F) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, count) = group + new_grouping[(region, axis, terrain, parallel, count)] = ([], []) + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, count) = group + (forward_edges, back_edges) = grouping[group] + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel, count)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(region, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + if world.mode[player] == 'standard': + # tuple goes to (A,B,C,D,E,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (std, region, axis, terrain, parallel, _) = group + new_grouping[(std, region, axis, terrain, parallel)] = ([], []) + + for group in grouping.keys(): + (std, region, axis, terrain, parallel, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, parallel)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(std, region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + + return list(new_grouping.values()) + else: + # tuple goes to (_,B,C,D,E,_) + for grouping in (groups,): + new_grouping = {} + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, _) = group + new_grouping[(region, axis, terrain, parallel)] = ([], []) + + for group in grouping.keys(): + (_, region, axis, terrain, parallel, _) = group + (forward_edges, back_edges) = grouping[group] + forward_edges = [[i] for l in forward_edges for i in l] + back_edges = [[i] for l in back_edges for i in l] + + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, parallel)] + exist_forward_edges.extend(forward_edges) + exist_back_edges.extend(back_edges) + new_grouping[(region, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: From b130b0b76bc39af725a2507af485fb02bfd12cd4 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 08:09:45 -0500 Subject: [PATCH 071/104] Fixed issue with Mixed and Crossed getting turned on incorrectly in Mystery --- Mystery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mystery.py b/Mystery.py index f92764d9..7efda4af 100644 --- a/Mystery.py +++ b/Mystery.py @@ -133,9 +133,9 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' - ret.ow_crossed = get_choice('overworld_crossed') ret.ow_keepsimilar = get_choice('overworld_keepsimilar') - ret.ow_mixed = get_choice('overworld_mixed') + ret.ow_crossed = get_choice('overworld_crossed') == 'on' + ret.ow_mixed = get_choice('overworld_mixed') == 'on' overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' entrance_shuffle = get_choice('entrance_shuffle') From a04184f3e6702cbc515565d2fbfa1e77f782e012 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 08:11:32 -0500 Subject: [PATCH 072/104] Fixed spoiler log to show Crossed OW in all cases --- BaseClasses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index a1d522e8..4bfc28dd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2393,7 +2393,6 @@ class Spoiler(object): outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla': - outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_crossed'][player] else 'No')) outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) outfile.write('Mixed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) From 94a70dbac927d37ee69a971f15eff26108044460 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 08:11:58 -0500 Subject: [PATCH 073/104] Fixed spoiler log to show Crossed OW in all cases --- BaseClasses.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BaseClasses.py b/BaseClasses.py index 4bfc28dd..abcae332 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2394,6 +2394,7 @@ class Spoiler(object): outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) if self.metadata['ow_shuffle'][player] != 'vanilla': outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No')) + outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_crossed'][player] else 'No')) outfile.write('Mixed OW:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_mixed'][player] else 'No')) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) From b2a6e4a7d48fc5145878755ef7e81149fc61c70d Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 20 Aug 2021 10:06:45 -0600 Subject: [PATCH 074/104] Update release notes --- RELEASENOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fab7cf7d..37877c89 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,10 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.0 + * Large logic refactor introducing a new method of key logic + * Some performance optimization + * Some outstanding bug fixes (boss shuffle "full" picks three unique bosses to be duplicated, e.g.) * 0.5.0.3 * Fixed a bug in retro+vanilla and big key placement * Fixed a problem with shops not registering in the Multiclient until you visit one From 80fc18146a58b6efb344b6ed2cce38d8efb7991f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 12:30:42 -0500 Subject: [PATCH 075/104] Fixed issue with Keep Similar getting turned on incorrectly in Mystery --- Mystery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mystery.py b/Mystery.py index 7efda4af..fa6bc992 100644 --- a/Mystery.py +++ b/Mystery.py @@ -133,7 +133,7 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' - ret.ow_keepsimilar = get_choice('overworld_keepsimilar') + ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on' ret.ow_crossed = get_choice('overworld_crossed') == 'on' ret.ow_mixed = get_choice('overworld_mixed') == 'on' overworld_flute = get_choice('flute_shuffle') From 22bbbaa52146ba588beed63328fa7d565daf0a51 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 12:33:19 -0500 Subject: [PATCH 076/104] Fixed issue with vanilla key logic check --- DoorShuffle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index 7c6250c0..917f536c 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -214,7 +214,7 @@ def vanilla_key_logic(world, 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) - if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and not world.owCrossed[player] and world.owMixed[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and not world.owCrossed[player] and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: validate_vanilla_key_logic(world, player) From f051e3a0b2a004ab614cb3a124ec1cde919805b0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 14:08:29 -0500 Subject: [PATCH 077/104] Some Inverted situations getting applied/removed in Mixed OW --- BaseClasses.py | 5 ++--- DoorShuffle.py | 8 +++++--- EntranceShuffle.py | 1 + Fill.py | 2 +- ItemList.py | 1 + KeyDoorShuffle.py | 2 +- Regions.py | 2 +- Rom.py | 1 + Rules.py | 2 +- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 5a1c25c0..b00c3afc 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -104,6 +104,7 @@ class World(object): self.__dict__.setdefault(attr, {})[player] = val set_player_attr('_region_cache', {}) set_player_attr('player_names', []) + set_player_attr('owswaps', [[],[],[]]) set_player_attr('remote_items', False) set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('swamp_patch_required', False) @@ -111,7 +112,7 @@ class World(object): set_player_attr('ganon_at_pyramid', True) set_player_attr('ganonstower_vanilla', True) set_player_attr('sewer_light_cone', self.mode[player] == 'standard') - set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') + set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or ((self.mode[player] == 'inverted') != (0x05 in self.owswaps[player][0] and self.owMixed[player]))) set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla']) set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple']) @@ -149,7 +150,6 @@ class World(object): set_player_attr('mixed_travel', 'prevent') set_player_attr('standardize_palettes', 'standardize') set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) - set_player_attr('owswaps', [[],[],[]]) set_player_attr('prizes', {'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0}) set_player_attr('exp_cache', defaultdict(dict)) @@ -3049,7 +3049,6 @@ class Settings(object): args.shufflepots[p] = True if settings[7] & 0x4 else False -@unique class KeyRuleType(FastEnum): WorstCase = 0 AllowSmall = 1 diff --git a/DoorShuffle.py b/DoorShuffle.py index 0f7ee939..94d7ff32 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -89,7 +89,7 @@ def link_doors_main(world, player): if world.mode[player] == 'standard': world.get_portal('Sanctuary', player).destination = True world.get_portal('Desert East', player).destination = True - if world.mode[player] == 'inverted': + if (world.mode[player] == 'inverted') != (0x30 in world.owswaps[player][0] and world.owMixed[player]): world.get_portal('Desert West', player).destination = True else: world.get_portal('Skull 2 West', player).destination = True @@ -1838,7 +1838,9 @@ def find_accessible_entrances(world, player, builder): elif world.mode[player] != 'inverted': start_regions = ['Links House', 'Sanctuary'] else: - start_regions = ['Links House', 'Dark Sanctuary Hint', 'Hyrule Castle Ledge'] + start_regions = ['Links House', 'Dark Sanctuary Hint'] + if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]): + start_regions.append('Hyrule Castle Ledge') regs = convert_regions(start_regions, world, player) visited_regions = set() visited_entrances = [] @@ -1853,7 +1855,7 @@ def find_accessible_entrances(world, player, builder): while len(queue) > 0: next_region = queue.popleft() visited_regions.add(next_region) - if world.mode[player] == 'inverted' and next_region.name == 'Tower Agahnim 1': + if (world.mode[player] == 'inverted') != (0x1b in world.owswaps[player][0] and world.owMixed[player]) and next_region.name == 'Tower Agahnim 1': connect = world.get_region('Hyrule Castle Ledge', player) if connect not in queue and connect not in visited_regions: queue.append(connect) diff --git a/EntranceShuffle.py b/EntranceShuffle.py index afd46fff..8fda03fc 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -1230,6 +1230,7 @@ def simple_shuffle_dungeons(world, player): dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Ganons Tower Exit') else: + # TODO: Should we be ignoring world.shuffle_ganon?? dungeon_entrances.append('Ganons Tower') dungeon_exits.append('Agahnims Tower Exit') diff --git a/Fill.py b/Fill.py index 43acd0e2..015760ca 100644 --- a/Fill.py +++ b/Fill.py @@ -697,7 +697,7 @@ def balance_money_progression(world): sphere_locations = get_sphere_locations(state, unchecked_locations) checked_locations = [] for player in range(1, world.players+1): - kiki_payable = state.prog_items[('Moon Pearl', player)] > 0 or world.mode[player] == 'inverted' + kiki_payable = state.prog_items[('Moon Pearl', player)] > 0 or (world.mode[player] == 'inverted') != (0x1e in world.owswaps[player][0] and world.owMixed[player]) if kiki_payable and world.get_region('Palace of Darkness Area', player) in state.reachable_regions[player]: if not kiki_paid[player]: kiki_check[player] = True diff --git a/ItemList.py b/ItemList.py index aac8a010..9e8b3245 100644 --- a/ItemList.py +++ b/ItemList.py @@ -398,6 +398,7 @@ def set_up_take_anys(world, player): if world.mode[player] == 'inverted': if 'Dark Sanctuary Hint' in take_any_locations: take_any_locations.remove('Dark Sanctuary Hint') + if (world.mode[player] == 'inverted') != (0x29 in world.owswaps[player][0] and world.owMixed[player]): if 'Archery Game' in take_any_locations: take_any_locations.remove('Archery Game') diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5a59180d..860e1d93 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -1898,7 +1898,7 @@ def val_mire(key_logic, world, player): def val_turtle(key_logic, world, player): # todo: check vanilla key logic when TR back doors are accessible - if world.shuffle[player] == 'vanilla' and world.mode[player] != 'inverted' and world.logic[player] in ('noglitches', 'minorglitches'): + if world.shuffle[player] == 'vanilla' and (world.mode[player] == 'inverted') == (0x05 in world.owswaps[player][0] and world.owMixed[player]) and world.logic[player] in ('noglitches', 'minorglitches'): val_rule(key_logic.door_rules['TR Hub NW'], 1) val_rule(key_logic.door_rules['TR Pokey 1 NW'], 2) val_rule(key_logic.door_rules['TR Chain Chomps Down Stairs'], 3) diff --git a/Regions.py b/Regions.py index a7907180..d2d977d5 100644 --- a/Regions.py +++ b/Regions.py @@ -1116,7 +1116,7 @@ def mark_dark_world_regions(world, player): def create_shops(world, player): world.shops[player] = [] for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram) in shop_table.items(): - if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop': + if (world.mode[player] == 'inverted') != (0x35 in world.owswaps[player][0] and world.owMixed[player]) and region_name == 'Dark Lake Hylia Shop': locked = True inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)] region = world.get_region(region_name, player) diff --git a/Rom.py b/Rom.py index f5435f31..67077dc6 100644 --- a/Rom.py +++ b/Rom.py @@ -2471,6 +2471,7 @@ def set_inverted_mode(world, player, rom, inverted_buffer): if world.doorShuffle[player] == 'vanilla' or world.intensity[player] < 3: write_int16(rom, 0x15AEE + 2*0x38, 0x00E0) write_int16(rom, 0x15AEE + 2*0x25, 0x000C) + if (world.mode[player] == 'inverted') != (0x03 in world.owswaps[player][0] and world.owMixed[player]): if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01]) # mountain cave starts on OW diff --git a/Rules.py b/Rules.py index 0404ab8f..c3842593 100644 --- a/Rules.py +++ b/Rules.py @@ -1491,8 +1491,8 @@ def swordless_rules(world, player): set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!) set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock Ledge', 'Region', player)) # sword not required to use medallion for opening in swordless (!) - add_bunny_rule(world.get_entrance('Turtle Rock', player), player) add_bunny_rule(world.get_entrance('Misery Mire', player), player) + add_bunny_rule(world.get_entrance('Turtle Rock', player), player) std_kill_rooms = { 'Hyrule Dungeon Armory Main': ['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], # One green guard From eb9dc8df75dd04241fdf2ec4e7898fc2178f5bc7 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 20:44:13 -0500 Subject: [PATCH 078/104] Fixed Flute crashes --- Rom.py | 2 +- asm/owrando.asm | 2 +- data/base2current.bps | Bin 141165 -> 141164 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rom.py b/Rom.py index 67077dc6..1001fb0b 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '6d8c7d1f0530c1ed1da5afa60e7a6d98' +RANDOMIZERBASEHASH = '23f0983f5dfd93490aacab9a60bc6a28' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 63107a0e..aca29b3b 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -353,7 +353,7 @@ OWNewDestination: ; turn into bunny lda $5d : cmp #$17 : beq .return lda #$17 : sta $5d - lda #$01 : sta $2e0 + lda #$01 : sta $02e0 bra .return .nobunny lda $5d : cmp #$17 : bne .return diff --git a/data/base2current.bps b/data/base2current.bps index 7246d99df4b6f3c211066d371982a23d9bbc0803..f717a4b10abbd4cb8ed3f68aaca62caa67bd0f18 100644 GIT binary patch delta 1634 zcmWkseNa;e5`X*h67oSq1mx=w!jnVGpduo8d`Q&l#Q;LFL#ZNQ3sY)5m=h2Xg!iJ6 z#w12q(-Z>a@l_hY2Ko-YUI!6+)a1FfpbW#T7+EYGHAOv^=3#gay3Uh_OC=woUPYujG z7sOuI=>KZaV6o_c->Z2pR@XuQzK2^Ri`+4KGyOG=iFQE{z7XAo6L`z(bWFl5cak;J znptkxOm`Z4)ix`A>|(Fmt+-^S7mU5j&2-)a&aG-{rj7W%nDNSb&M_4I5TA();SL^> z>x`09^ITETMT5^Y=ig*_ii)HG zxreQ)-*A75BTSxy@!uo`13bXFt^o6zfgu)!P6S5a zEE^=ySREd?{cUz^k(wfhC8bVp4)TNNgr$j0JWf<=APc=~cECHRswt5AJkd^(#7w%< z2S2ST1vRd#T`w0TzPzd)7+2|jr7gtx+V?zO{kS$1GI6xF7GiMQnJ`GkLucOJVn32N z-Qu<5+|?QJYH9n|l#|e|mUc&xGZR|c+s)^ zYN;o7J%V#kS`4&YVBA>Sd?!(kFZ3hN+dB1gO8XJb z3%&1X%ele26RfqUkLV}H`M5GxKN)LJWAuWb?7hZ##^(1cY)e1XWZQ$g8}_VMt+3S& z9b0YDIX0|V-d%oHCHg!auQW_}OCGhP9n`;VJ23MUs|bBCumD;5f#S1sq99NQ=yyC z+K%$MLxmvp&0%^}_?N2}BpZ*KE`_8rjs*VTXEmf~Ys-FyUcuYL+d;tpJp83Vn3iR7 z$oVh+)K1ByHB7T&ikgx22>7Be?g?ZED^0BQlbxykw(WfCSP6)Cmrd(ueC_o(aN`an z^3~&NfNK8c%_!18FvvVAa{^dm=mtv;-QfC-Er(y7^cF(|dVQ;blYGv%T>`)38Ric9 z!j0ZJrK=o&t7mCP05AG(XC&EBi>XVs5XE0#(ujmqIcqz}i&pa_u+>&5fISd(y-NVw gNO;foy%1>7G%X9^`m&JxV}v;2P2IHd-9GvM0ZxXm$N&HU delta 1657 zcmWksdsI`08Nc66Zb$;ADGxzfAzTMIn3X>59D{rLX-e_cj z#2CKD91|f|9%6Vj=&hwa9YvU}MmL*zx^s4lJ?qlSY*ri=i;nf!rQi3*?{~g)zW49D zJK}{q;)z#5*zpQ;2lHV!OyWE6K8X2TG7v~&fiD(N0K2eE_ym5$ebh5~%02EoJv+}u zu#-8u>-t=*6zvZAah^-m_R`q{+#*@wOVAnV8#pCOg*CV=x&vzbB%moFWsW<{8tL3Q zZo)|Sbq}hn7P_=+(C3q%Hqr~-gOAPhZ|`xw%uX|Hz(ZmtpzsmL2x%`q6qmza@HI&h zjN=7KjC|SOyrMgI^oCSl>@d>5!@H6+$?ZALSIC%Yz%Xg7*tEzcuon7nm?_;T^Vgx* zE^uegdHrM@k4xiWGxkfj1y0=Oe7-Te$w)8r52WoRkbKpuCuI1#@9oN#a|2NuIzQ0S z**7AEXB!wD9n9Cu{|7H#y3fURW5L0mRDa&=mt2K^Sw9pqe!#BaWVnmhg3F?If6ECS zdfN6cYvhZk^gjQVT62l{Th6zh{b$Kbm>#kY=J?8xzIYim&s9Wp>7TpBg?8!}P@Xm- z^G};OtpD825eEOk_?u~K0KP=;whX93I^6@`;8OZdh~!h0w#{&zzgwpzgWM%$E|E*X zjQmnaVEM7Fl&bg}j7HnB1Jb%Vx9|+4sTFnj>{vZjyqCdjY^whi{2C|f_dzKJG(^Ex zjBil--(T5~8W`hd<4ozy(8%;O4mZ%@TW_+VwtezbY!LR~5x03l+_o}`wfI%T??d-k zVi?7&{BvT6RJm%{M2{fPhNX>hk$FYHSX^fdJJQljMvGIcBb?f8%u{_{7up*m17h|8 zldFx#KQ_|PkN;?V9c-A?v;n%Xtmy(A>1ocV{Hn4iU{P~ySjWuNJHV{cGsGh0L|_EY z+EEg_nxn&$_OnxqLN_@fsdff&P_o8LRi`jp@mg~Z?7_g6BshQ>EoD;q1GSqZJh=)v z4!2Z84$51@WK_zNan;ClruOHlMVQq3X4K(CNZS3!^cn>$IG-n~kkA?L~UE7fYR!a!zSe6H$>ZYgmisY3R{sWMA}}UflaUgDShz|8H`T&y?wBI3u6uWnc8w+>$L7frDjK1)(>p6L(4Xsw2tT> zOqGuxHj1ufBWh-X0>19aDAnpp6uE3UU(sKom? z5l)gYX$m{=E!^La0ei#-_M~GkKI)GSI3zK!e{j_B;R7@Y&6qaifDHbNp#N{SvBVG$~<$+&&=ajYcDfMk?` zRpCb#VYL&~)z;|x?jd(!hhM{BOp}feI~R2Dame Date: Fri, 20 Aug 2021 22:48:05 -0500 Subject: [PATCH 079/104] Fixed Bunny/Lonk issues in Crossed OW --- Rom.py | 2 +- asm/owrando.asm | 7 +++---- data/base2current.bps | Bin 141164 -> 141151 bytes 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Rom.py b/Rom.py index 1001fb0b..51a7ac10 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '23f0983f5dfd93490aacab9a60bc6a28' +RANDOMIZERBASEHASH = '363c49821f25327f10bc200c98375bde' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index aca29b3b..300d4da8 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -351,13 +351,12 @@ OWNewDestination: .inverted lda $7ef3ca : and.b #$40 : eor #$40 + cmp #$40 : bne .nobunny ; turn into bunny - lda $5d : cmp #$17 : beq .return lda #$17 : sta $5d - lda #$01 : sta $02e0 + lda #$01 : sta $02e0 : sta $56 bra .return + .nobunny - lda $5d : cmp #$17 : bne .return - stz $5d : stz $2e0 + stz $5d : stz $02e0 : stz $56 .return lda $05 : sta $8a diff --git a/data/base2current.bps b/data/base2current.bps index f717a4b10abbd4cb8ed3f68aaca62caa67bd0f18..17f77f5f1b222671e41c05764cdc4c3b6cecbbd4 100644 GIT binary patch delta 111 zcmV-#0FeLe&IsSm2(U>31RXWv%7aY-w@m>75d}FurUjp3WA&0HvVkX)7l(lemn!Fh zfSU>trj31phDq&x1_?w@m>75d}s@rUjp3WA&0HvVkj;7l(lemn!Fh zfSU>trmaap$w1H%rCrGv@D`&rc&43z3#DDj7tjTh2Y{KS1%-;jFkqt}O7uVA9=DeU e0nShX7nf#N0W<+Qx0_c1yw3 From 365f11564a2fd857d7c7e3da961eeca68e88d7db Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 22:56:14 -0500 Subject: [PATCH 080/104] Version bump 0.1.8.2 --- CHANGELOG.md | 4 ++++ OverworldShuffle.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b287ea8..fa5419be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 0.1.8.2 +- Fixed issue with game crashing on using Flute +- Fixed issues with Link/Bunny state in Crossed OW + ### 0.1.8.1 - Fixed issue with activating flute in DW (OW Mixed) - Fixed issue with Parallel+Crossed not generating diff --git a/OverworldShuffle.py b/OverworldShuffle.py index bf3217e2..05c3d35a 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.8.1-u' +__version__ = '0.1.8.2-u' def link_overworld(world, player): # setup mandatory connections From 3da44b6b94c6263f5b3697172ccfdb4e67009974 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 20 Aug 2021 23:01:56 -0500 Subject: [PATCH 081/104] Changelog update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5419be..79c7b2c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ### 0.1.8.2 - Fixed issue with game crashing on using Flute - Fixed issues with Link/Bunny state in Crossed OW +- Fixed issue with Standard+Parallel not using vanilla connections for Escape +- Fixed issue with Mystery for OW boolean options +- ~~Merged DR v0.5.1.0 - Major Keylogic Update~~ + ### 0.1.8.1 - Fixed issue with activating flute in DW (OW Mixed) From d1b0b57d533b6811aa2f7369481f72d9396de3cf Mon Sep 17 00:00:00 2001 From: codemann8 Date: Sat, 21 Aug 2021 18:19:57 -0500 Subject: [PATCH 082/104] Retain Link state on transition in Crossed OW unless bunny --- Rom.py | 2 +- asm/owrando.asm | 9 ++++++--- data/base2current.bps | Bin 141151 -> 141164 bytes 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Rom.py b/Rom.py index 51a7ac10..5dc3e361 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '363c49821f25327f10bc200c98375bde' +RANDOMIZERBASEHASH = '261aa02eb4ee7e56e626361f170de5f4' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 300d4da8..07173255 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -351,12 +351,15 @@ OWNewDestination: .inverted lda $7ef3ca : and.b #$40 : eor #$40 + cmp #$40 : bne .nobunny ; turn into bunny - lda #$17 : sta $5d - lda #$01 : sta $02e0 : sta $56 + lda $5d : cmp #$04 : beq + ; if swimming, continue + lda #$17 : sta $5d + + lda #$01 : sta $02e0 : sta $56 bra .return .nobunny - stz $5d : stz $02e0 : stz $56 + lda $5d : cmp #$17 : bne + ; retain current state unless bunny + stz $5d + + stz $02e0 : stz $56 .return lda $05 : sta $8a diff --git a/data/base2current.bps b/data/base2current.bps index 17f77f5f1b222671e41c05764cdc4c3b6cecbbd4..512a6dc9d18d1f1cfd322a5900c2cf095832b4e8 100644 GIT binary patch delta 124 zcmV-?0E7SE&Is(z2(U>31c)zp(1T3@w@m>75d}s{rUjp3WA&0HvVkp=7l(lemn!Fh zfSU>trmaap$w1H(rCrGc@C2(hc&43z4W(Vl7tjKm3xJuW1%-;jFkqu2O7t(_CbyRc e0nShX50_?F0W<+Yx0_c1yw3=kE9FaYHnYlZoHH>1 delta 111 zcmV-#0FeLe&IsSm2(U>31RXWv%7aY-w@m>75d}FurUjp3WA&0HvVkX)7l(lemn!Fh zfSU>trj Date: Tue, 24 Aug 2021 02:39:24 -0500 Subject: [PATCH 083/104] Expanded and restructured OW Shuffle to include more Crossed options --- BaseClasses.py | 2 +- CLI.py | 2 +- DoorShuffle.py | 2 +- Main.py | 2 +- Mystery.py | 2 +- OverworldShuffle.py | 707 ++++++++---------- Plando.py | 3 +- Plandomizer_Template.txt | 2 +- Rom.py | 6 +- Rules.py | 2 +- resources/app/cli/args.json | 9 +- resources/app/cli/lang/en.json | 10 +- resources/app/gui/lang/en.json | 5 + .../app/gui/randomize/overworld/widgets.json | 11 +- 14 files changed, 344 insertions(+), 421 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b00c3afc..71dbcddc 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2980,7 +2980,7 @@ class Settings(object): @staticmethod def make_code(w, p): code = bytes([ - (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]], + (dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] != 'none' else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]], (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) | (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0), diff --git a/CLI.py b/CLI.py index ccc66e00..1d0f9a43 100644 --- a/CLI.py +++ b/CLI.py @@ -146,7 +146,7 @@ def parse_settings(): "openpyramid": False, "shuffleganon": True, "ow_shuffle": "vanilla", - "ow_crossed": False, + "ow_crossed": "none", "ow_keepsimilar": False, "ow_mixed": False, "ow_fluteshuffle": "vanilla", diff --git a/DoorShuffle.py b/DoorShuffle.py index 94d7ff32..c6644f35 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -215,7 +215,7 @@ def vanilla_key_logic(world, 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) - # if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and not world.owCrossed[player] and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: + # if world.shuffle[player] == 'vanilla' and world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'none' and not world.owMixed[player] and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) diff --git a/Main.py b/Main.py index 94dc4b9a..407205e4 100644 --- a/Main.py +++ b/Main.py @@ -263,7 +263,7 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] or world.owMixed[1] or str(world.seed).startswith('M'): + if world.owShuffle[1] != 'vanilla' or world.owCrossed[1] != 'none' or world.owMixed[1] or str(world.seed).startswith('M'): outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' else: outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' diff --git a/Mystery.py b/Mystery.py index 65c506c9..702a67d5 100644 --- a/Mystery.py +++ b/Mystery.py @@ -135,8 +135,8 @@ def roll_settings(weights): overworld_shuffle = get_choice('overworld_shuffle') ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla' + ret.ow_crossed = get_choice('overworld_crossed') ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on' - ret.ow_crossed = get_choice('overworld_crossed') == 'on' ret.ow_mixed = get_choice('overworld_mixed') == 'on' overworld_flute = get_choice('flute_shuffle') ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla' diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 05c3d35a..da55a883 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -11,10 +11,108 @@ def link_overworld(world, player): for exitname, destname in temporary_mandatory_connections: connect_two_way(world, exitname, destname, player) + def performSwap(groups, swaps): + def getParallel(edgename): + if edgename in parallel_links: + return parallel_links[edgename] + elif edgename in parallel_links.inverse: + return parallel_links.inverse[edgename][0] + else: + raise Exception('No parallel edge found for edge %s', edgename) + + def getNewSets(all_set, other_set): + new_all_set = list(map(getParallel, all_set)) + if not all(edge in orig_swaps for edge in new_all_set): + raise Exception('Cannot move a parallel edge without the other') + else: + for edge in new_all_set: + swaps.remove(edge) + new_other_set = getNewSet(other_set) + return (new_all_set, new_other_set) + + def getNewSet(edge_set): + new_set = [] + for edge in edge_set: + if edge in orig_swaps: + new_edge = getParallel(edge) + if new_edge not in orig_swaps: + raise Exception('Cannot move a parallel edge without the other') + new_set.append(new_edge) + swaps.remove(new_edge) + else: + new_set.append(edge) + return new_set + + # swaps edges from one pool to another + orig_swaps = copy.deepcopy(swaps) + new_groups = {} + for group in groups.keys(): + new_groups[group] = ([],[]) + + for group in groups.keys(): + (mode, wrld, dir, terrain, parallel, count) = group + for (forward_set, back_set) in zip(groups[group][0], groups[group][1]): + anyF = any(edge in orig_swaps for edge in forward_set) + anyB = any(edge in orig_swaps for edge in back_set) + allF = all(edge in orig_swaps for edge in forward_set) + allB = all(edge in orig_swaps for edge in back_set) + if not (anyF or anyB): + # no change + new_groups[group][0].append(forward_set) + new_groups[group][1].append(back_set) + elif allF and allB: + # move both sets + if parallel == IsParallel.Yes and not (all(edge in orig_swaps for edge in map(getParallel, forward_set)) and all(edge in orig_swaps for edge in map(getParallel, back_set))): + raise Exception('Cannot move a parallel edge without the other') + new_groups[(OpenStd.Open, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][0].append(forward_set) + new_groups[(OpenStd.Open, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][1].append(back_set) + for edge in forward_set: + swaps.remove(edge) + for edge in back_set: + swaps.remove(edge) + elif anyF or anyB: + if parallel == IsParallel.Yes: + if allF or allB: + # move one set + if allF and not (world.owKeepSimilar[player] and anyB): + (new_forward_set, new_back_set) = getNewSets(forward_set, back_set) + elif allB and not (world.owKeepSimilar[player] and anyF): + (new_back_set, new_forward_set) = getNewSets(back_set, forward_set) + else: + raise Exception('Cannot move an edge out of a Similar group') + new_groups[group][0].append(new_forward_set) + new_groups[group][1].append(new_back_set) + else: + # move individual edges + if not world.owKeepSimilar[player]: + new_groups[group][0].append(getNewSet(forward_set) if anyF else forward_set) + new_groups[group][1].append(getNewSet(back_set) if anyB else back_set) + else: + raise Exception('Cannot move an edge out of a Similar group') + else: + raise NotImplementedError('Cannot move one side of a non-parallel connection') + else: + raise NotImplementedError('Invalid OW Edge swap scenario') + groups = new_groups + + tile_groups = reorganize_tile_groups(world, player) trimmed_groups = copy.deepcopy(OWEdgeGroups) + swapped_edges = list() - # adjust Frog/Dig Game swap manually due to NP/P relationship with LW - if world.owShuffle[player] == 'parallel' and not world.owKeepSimilar[player]: + # restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship + if world.owKeepSimilar[player]: + for group in trimmed_groups.keys(): + (std, region, axis, terrain, parallel, _) = group + if parallel == IsParallel.Yes: + (forward_edges, back_edges) = trimmed_groups[group] + if ['Maze Race ES'] in forward_edges: + forward_edges = list(filter((['Maze Race ES']).__ne__, forward_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][0].append(['Maze Race ES']) + if ['Kakariko Suburb WS'] in back_edges: + back_edges = list(filter((['Kakariko Suburb WS']).__ne__, back_edges)) + trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1)][1].append(['Kakariko Suburb WS']) + trimmed_groups[group] = (forward_edges, back_edges) + else: for group in trimmed_groups.keys(): (std, region, axis, terrain, _, _) = group (forward_edges, back_edges) = trimmed_groups[group] @@ -29,131 +127,17 @@ def link_overworld(world, player): trimmed_groups[group] = (forward_edges, back_edges) # tile shuffle + logging.getLogger('').debug('Swapping overworld tiles') if world.owMixed[player]: - tile_groups = {} - for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: - tile_groups[(name,)] = ([], [], []) - else: - tile_groups[(name, groupType)] = ([], [], []) - - for (name, groupType) in OWTileGroups.keys(): - if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: - (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] - if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: - (exist_owids, exist_lw_regions, exist_dw_regions) = tile_groups[(name,)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - tile_groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) - else: - (exist_owids, exist_lw_regions, exist_dw_regions) = tile_groups[(name, groupType)] - exist_owids.extend(lw_owids) - exist_owids.extend(dw_owids) - for owid in lw_owids: - exist_lw_regions.extend(OWTileRegions.inverse[owid]) - for owid in dw_owids: - exist_dw_regions.extend(OWTileRegions.inverse[owid]) - tile_groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) + swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], player) - # tile shuffle happens here, the groups that remain in the list are the tiles that get swapped - removed = list() - for group in tile_groups.keys(): - if random.randint(0, 1): - removed.append(group) - for group in removed: - tile_groups.pop(group, None) + # move swapped regions/edges to other world + performSwap(trimmed_groups, swapped_edges) + assert len(swapped_edges) == 0, 'Not all edges were swapped successfully: ' + ', '.join(swapped_edges ) - # save shuffled tiles to world object - for group in tile_groups.keys(): - (owids, lw_regions, dw_regions) = tile_groups[group] - (exist_owids, exist_lw_regions, exist_dw_regions) = world.owswaps[player] - exist_owids.extend(owids) - exist_lw_regions.extend(lw_regions) - exist_dw_regions.extend(dw_regions) - world.owswaps[player] = [exist_owids, exist_lw_regions, exist_dw_regions] - - # replace LW edges with DW - ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool - for edgeset in temporary_mandatory_connections: - for edge in edgeset: - ignore_list.append(edge) - - swapped_edges = list() - def getSwappedEdges(world, lst, player): - for regionname in lst: - region = world.get_region(regionname, player) - for exit in region.exits: - if exit.spot_type == 'OWEdge' and exit.name not in ignore_list: - swapped_edges.append(exit.name) - - getSwappedEdges(world, world.owswaps[player][1], player) - getSwappedEdges(world, world.owswaps[player][2], player) - - def performSwap(groups, swaps, nonParallelOnly=False): - try: - for group in groups.keys(): - (mode, wrld, dir, terrain, parallel, count) = group - for p in range(0, len(groups[group])): - edgepool = groups[group][p] - for s in range(0, len(edgepool)): - if s <= len(edgepool): - for e in range(0, len(edgepool[s])): - if len(edgepool) > 0 and edgepool[s][e] in swaps: - if parallel == IsParallel.Yes: - if not nonParallelOnly: - if wrld == WorldType.Light and edgepool[s][e] in parallel_links: - logging.getLogger('').debug('%s was moved', edgepool[s][e]) - swaps.remove(edgepool[s][e]) - groups[group][p][s][e] = parallel_links[edgepool[s][e]] - elif wrld == WorldType.Dark and edgepool[s][e] in parallel_links.inverse: - logging.getLogger('').debug('%s was moved', edgepool[s][e]) - swaps.remove(edgepool[s][e]) - groups[group][p][s][e] = parallel_links.inverse[edgepool[s][e]][0] - else: - for edge in edgepool[s]: - logging.getLogger('').debug('%s was moved', edge) - swaps.remove(edge) - groups[(mode, WorldType((int(wrld) + 1) % 2), dir, terrain, parallel, count)][p].append(edgepool[s]) - groups[group][p].remove(edgepool[s]) - except IndexError: - #TODO: Figure out a way to handle index changes on the fly when removing items - logging.getLogger('').warning('OW Tile Swap encountered minor IndexError... retrying') - - if world.owShuffle[player] != 'parallel' and 0x28 in world.owswaps[player][0]: # handle Frog/Dig Game swap manually due to NP/P relationship with LW - trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][0].append(['Maze Race ES']) - trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][1].append(['Kakariko Suburb WS']) - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][0].remove(['Maze Race ES']) - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.Yes, 1)][1].remove(['Kakariko Suburb WS']) - - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.No, 2)][0].append(['Dig Game EC', 'Dig Game ES']) - trimmed_groups[(OpenStd.Open, WorldType.Light, PolSlot.EastWest, Terrain.Land, IsParallel.No, 2)][1].append(['Frog WC', 'Frog WS']) - trimmed_groups[(OpenStd.Open, WorldType.Dark, PolSlot.EastWest, Terrain.Land, IsParallel.No, 2)] = [[],[]] - - swapped_edges.remove('Maze Race ES') - swapped_edges.remove('Kakariko Suburb WS') - swapped_edges.remove('Dig Game EC') - swapped_edges.remove('Dig Game ES') - swapped_edges.remove('Frog WC') - swapped_edges.remove('Frog WS') - - tries = 5 - while tries > 0: - performSwap(trimmed_groups, swapped_edges) - if len(swapped_edges) == 0: - tries = 0 - continue - tries -= 1 - assert len(swapped_edges) == 0 - - #move swapped regions to other world update_world_regions(world, player) - # make new connections + # apply tile logical connections for owid in ow_connections.keys(): if (world.mode[player] == 'inverted') == (owid in world.owswaps[player][0] and world.owMixed[player]): for (exitname, regionname) in ow_connections[owid][0]: @@ -162,10 +146,50 @@ def link_overworld(world, player): for (exitname, regionname) in ow_connections[owid][1]: connect_simple(world, exitname, regionname, player) - connected_edges = [] + # crossed shuffle + logging.getLogger('').debug('Crossing overworld edges') + if world.owCrossed[player] in ['grouped', 'limited']: + if world.owCrossed[player] == 'grouped': + crossed_edges = shuffle_tiles(world, tile_groups, [[],[],[]], player) + elif world.owCrossed[player] in ['limited', 'chaos']: + crossed_edges = list() + crossed_candidates = list() + for group in trimmed_groups.keys(): + (mode, wrld, dir, terrain, parallel, count) = group + if parallel == IsParallel.Yes and wrld == WorldType.Light and (mode == OpenStd.Open or world.mode[player] != 'standard'): + for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): + if world.owKeepSimilar[player]: + if world.owCrossed[player] == 'chaos' and random.randint(0, 1): + for edge in forward_set: + crossed_edges.append(edge) + elif world.owCrossed[player] == 'limited': + crossed_candidates.append(forward_set) + else: + for edge in forward_set: + if world.owCrossed[player] == 'chaos' and random.randint(0, 1): + crossed_edges.append(edge) + elif world.owCrossed[player] == 'limited': + crossed_candidates.append(forward_set) + break + if world.owCrossed[player] == 'limited': + random.shuffle(crossed_candidates) + for edge_set in crossed_candidates[:9]: + for edge in edge_set: + crossed_edges.append(edge) + for edge in copy.deepcopy(crossed_edges): + if edge in parallel_links: + crossed_edges.append(parallel_links[edge]) + elif edge in parallel_links.inverse: + crossed_edges.append(parallel_links.inverse[edge][0]) + + performSwap(trimmed_groups, crossed_edges) + assert len(crossed_edges) == 0, 'Not all edges were crossed successfully: ' + ', '.join(crossed_edges) # layout shuffle - if world.owShuffle[player] == 'vanilla' and not world.owCrossed[player]: + logging.getLogger('').debug('Shuffling overworld layout') + connected_edges = [] + + if world.owShuffle[player] == 'vanilla': # vanilla transitions groups = list(trimmed_groups.values()) for (forward_edge_sets, back_edge_sets) in groups: @@ -174,100 +198,61 @@ def link_overworld(world, player): assert len(forward_set) == len(back_set) for (forward_edge, back_edge) in zip(forward_set, back_set): connect_two_way(world, forward_edge, back_edge, player, connected_edges) - - assert len(connected_edges) == len(default_connections) * 2, connected_edges else: - if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel': + if world.owKeepSimilar[player] and world.owShuffle[player] in ['vanilla', 'parallel']: for exitname, destname in parallelsimilar_connections: connect_two_way(world, exitname, destname, player, connected_edges) - if world.owShuffle[player] == 'vanilla' and world.owCrossed[player]: - if world.mode[player] == 'standard': - # connect vanilla std - for group in trimmed_groups.keys(): - (std, _, _, _, _, _) = group - if std == OpenStd.Standard: - (forward_set, back_set) = trimmed_groups[group] - for (forward_edges, back_edges) in zip(forward_set, back_set): - for (forward_edge, back_edge) in zip(forward_edges, back_edges): - connect_two_way(world, forward_edge, back_edge, player, connected_edges) - - # connect non-parallel edges - for group in trimmed_groups.keys(): - (_, _, _, _, parallel, _) = group - if parallel == IsParallel.No: - (forward_set, back_set) = trimmed_groups[group] - for (forward_edges, back_edges) in zip(forward_set, back_set): - for (forward_edge, back_edge) in zip(forward_edges, back_edges): - if forward_edge not in connected_edges and back_edge not in connected_edges: - connect_two_way(world, forward_edge, back_edge, player, connected_edges) - #TODO: Remove, just for testing for exitname, destname in test_connections: connect_two_way(world, exitname, destname, player, connected_edges) connect_custom(world, connected_edges, player) + # layout shuffle trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) - groups = reorganize_groups(world, trimmed_groups, player) - # all layout shuffling occurs here - if world.owShuffle[player] != 'vanilla': - # layout shuffle - if world.mode[player] == 'standard': - random.shuffle(groups[2:]) # keep first 2 groups (Standard) first - else: - random.shuffle(groups) - - for (forward_edge_sets, back_edge_sets) in groups: - assert len(forward_edge_sets) == len(back_edge_sets) - random.shuffle(forward_edge_sets) - random.shuffle(back_edge_sets) - if len(forward_edge_sets) > 0: - f = 0 - b = 0 - while f < len(forward_edge_sets) and b < len(back_edge_sets): - forward_set = forward_edge_sets[f] - back_set = back_edge_sets[b] - while forward_set[0] in connected_edges: - f += 1 - if f < len(forward_edge_sets): - forward_set = forward_edge_sets[f] - else: - forward_set = None - break - f += 1 - while back_set[0] in connected_edges: - b += 1 - if b < len(back_edge_sets): - back_set = back_edge_sets[b] - else: - back_set = None - break - b += 1 - if forward_set is not None and back_set is not None: - assert len(forward_set) == len(back_set) - for (forward_edge, back_edge) in zip(forward_set, back_set): - connect_two_way(world, forward_edge, back_edge, player, connected_edges) - elif forward_set is not None: - logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) - elif back_set is not None: - logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) + if world.mode[player] == 'standard': + random.shuffle(groups[2:]) # keep first 2 groups (Standard) first else: - # vanilla/crossed shuffle - for (forward_edge_sets, back_edge_sets) in groups: - assert len(forward_edge_sets) == len(back_edge_sets) - for (forward_set, back_set) in zip(forward_edge_sets, back_edge_sets): - assert len(forward_set) == len(back_set) - swapped = random.randint(0, 1) - for (forward_edge, back_edge) in zip(forward_set, back_set): - if forward_edge not in connected_edges and back_edge not in connected_edges: - if swapped: - forward_edge = parallel_links[forward_edge] if forward_edge in parallel_links else parallel_links.inverse[forward_edge][0] + random.shuffle(groups) + + for (forward_edge_sets, back_edge_sets) in groups: + assert len(forward_edge_sets) == len(back_edge_sets) + random.shuffle(forward_edge_sets) + random.shuffle(back_edge_sets) + if len(forward_edge_sets) > 0: + f = 0 + b = 0 + while f < len(forward_edge_sets) and b < len(back_edge_sets): + forward_set = forward_edge_sets[f] + back_set = back_edge_sets[b] + while forward_set[0] in connected_edges: + f += 1 + if f < len(forward_edge_sets): + forward_set = forward_edge_sets[f] + else: + forward_set = None + break + f += 1 + while back_set[0] in connected_edges: + b += 1 + if b < len(back_edge_sets): + back_set = back_edge_sets[b] + else: + back_set = None + break + b += 1 + if forward_set is not None and back_set is not None: + assert len(forward_set) == len(back_set) + for (forward_edge, back_edge) in zip(forward_set, back_set): connect_two_way(world, forward_edge, back_edge, player, connected_edges) - - assert len(connected_edges) == len(default_connections) * 2, connected_edges + elif forward_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % forward_set[0]) + elif back_set is not None: + logging.getLogger('').warning("Edge '%s' could not find a valid connection" % back_set[0]) + assert len(connected_edges) == len(default_connections) * 2, connected_edges # flute shuffle def connect_flutes(flute_destinations): @@ -352,7 +337,20 @@ def connect_simple(world, exitname, regionname, player): def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): edge1 = world.get_entrance(edgename1, player) edge2 = world.get_entrance(edgename2, player) - + x = world.check_for_owedge(edgename1, player) + y = world.check_for_owedge(edgename2, player) + + if x is None: + raise Exception('%s is not a valid edge.', edgename1) + elif y is None: + raise Exception('%s is not a valid edge.', edgename2) + if connected_edges is not None: + if edgename1 in connected_edges or edgename2 in connected_edges: + if (x.dest and x.dest.name == edgename2) and (y.dest and y.dest.name == edgename1): + return + else: + raise Exception('Edges \'%s\' and \'%s\' already connected elsewhere', edgename1, edgename2) + # if these were already connected somewhere, remove the backreference if edge1.connected_region is not None: edge1.connected_region.entrances.remove(edge1) @@ -361,17 +359,10 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): edge1.connect(edge2.parent_region) edge2.connect(edge1.parent_region) - x = world.check_for_owedge(edgename1, player) - y = world.check_for_owedge(edgename2, player) - if x is None: - logging.getLogger('').error('%s is not a valid edge.', edgename1) - elif y is None: - logging.getLogger('').error('%s is not a valid edge.', edgename2) - else: - x.dest = y - y.dest = x + x.dest = y + y.dest = x - if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player]: + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': world.spoiler.set_overworld(edgename2, edgename1, 'both', player) if connected_edges is not None: @@ -379,7 +370,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): connected_edges.append(edgename2) # connecting parallel connections - if world.owShuffle[player] == 'parallel' or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player]): + if world.owShuffle[player] in ['vanilla', 'parallel']: if (edgename1 in parallel_links.keys() or edgename1 in parallel_links.inverse.keys()): try: parallel_forward_edge = parallel_links[edgename1] if edgename1 in parallel_links.keys() else parallel_links.inverse[edgename1][0] @@ -390,6 +381,77 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None): # TODO: Figure out why non-parallel edges are getting into parallel groups raise KeyError('No parallel edge for edge %s' % edgename2) +def shuffle_tiles(world, groups, result_list, player): + swapped_edges = list() + + # tile shuffle happens here + removed = list() + for group in groups.keys(): + if random.randint(0, 1): + removed.append(group) + + # save shuffled tiles to list + for group in groups.keys(): + if group not in removed: + (owids, lw_regions, dw_regions) = groups[group] + (exist_owids, exist_lw_regions, exist_dw_regions) = result_list + exist_owids.extend(owids) + exist_lw_regions.extend(lw_regions) + exist_dw_regions.extend(dw_regions) + result_list = [exist_owids, exist_lw_regions, exist_dw_regions] + + # replace LW edges with DW + ignore_list = list() #TODO: Remove ignore_list when special OW areas are included in pool + for edgeset in temporary_mandatory_connections: + for edge in edgeset: + ignore_list.append(edge) + + if world.owCrossed[player] != 'polar': + # in polar, the actual edge connections remain vanilla + def getSwappedEdges(world, lst, player): + for regionname in lst: + region = world.get_region(regionname, player) + for exit in region.exits: + if exit.spot_type == 'OWEdge' and exit.name not in ignore_list: + swapped_edges.append(exit.name) + + getSwappedEdges(world, result_list[1], player) + getSwappedEdges(world, result_list[2], player) + + return swapped_edges + +def reorganize_tile_groups(world, player): + groups = {} + for (name, groupType) in OWTileGroups.keys(): + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: + if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: + groups[(name,)] = ([], [], []) + else: + groups[(name, groupType)] = ([], [], []) + + for (name, groupType) in OWTileGroups.keys(): + if world.mode[player] != 'standard' or name not in ['Castle', 'Links', 'Central Bonk Rocks']: + (lw_owids, dw_owids) = OWTileGroups[(name, groupType,)] + if world.shuffle[player] in ['vanilla', 'simple', 'dungeonssimple']: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name,)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name,)] = (exist_owids, exist_lw_regions, exist_dw_regions) + else: + (exist_owids, exist_lw_regions, exist_dw_regions) = groups[(name, groupType)] + exist_owids.extend(lw_owids) + exist_owids.extend(dw_owids) + for owid in lw_owids: + exist_lw_regions.extend(OWTileRegions.inverse[owid]) + for owid in dw_owids: + exist_dw_regions.extend(OWTileRegions.inverse[owid]) + groups[(name, groupType)] = (exist_owids, exist_lw_regions, exist_dw_regions) + return groups + def remove_reserved(world, groupedlist, connected_edges, player): new_grouping = {} for group in groupedlist.keys(): @@ -405,7 +467,7 @@ def remove_reserved(world, groupedlist, connected_edges, player): back_edges = list(list(filter((edge).__ne__, i)) for i in back_edges) # remove parallel edges from pool, since they get added during shuffle - if (not world.owCrossed[player] and world.owShuffle[player] == 'parallel') and region == WorldType.Dark: + if world.owShuffle[player] == 'parallel' and region == WorldType.Dark: for edge in parallel_links: forward_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in forward_edges) back_edges = list(list(filter((parallel_links[edge]).__ne__, i)) for i in back_edges) @@ -428,250 +490,87 @@ def reorganize_groups(world, groups, player): # predefined shuffle groups get reorganized here # this restructures the candidate pool based on the chosen settings if world.owShuffle[player] == 'full': - if world.owCrossed[player]: - if world.owKeepSimilar[player]: - if world.mode[player] == 'standard': - # tuple goes to (A,_,C,D,_,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, _, axis, terrain, _, count) = group - new_grouping[(std, axis, terrain, count)] = ([], []) - - for group in grouping.keys(): - (std, _, axis, terrain, _, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,_,C,D,_,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, _, axis, terrain, _, count) = group - new_grouping[(axis, terrain, count)] = ([], []) - - for group in grouping.keys(): - (_, _, axis, terrain, _, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(axis, terrain, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.mode[player] == 'standard': - # tuple goes to (A,_,C,D,_,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, _, axis, terrain, _, _) = group - new_grouping[(std, axis, terrain)] = ([], []) - - for group in grouping.keys(): - (std, _, axis, terrain, _, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, axis, terrain)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,_,C,D,_,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, _, axis, terrain, _, _) = group - new_grouping[(axis, terrain)] = ([], []) - - for group in grouping.keys(): - (_, _, axis, terrain, _, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(axis, terrain)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.owKeepSimilar[player]: - if world.mode[player] == 'standard': - # tuple goes to (A,B,C,D,_,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, region, axis, terrain, _, count) = group - new_grouping[(std, region, axis, terrain, count)] = ([], []) - - for group in grouping.keys(): - (std, region, axis, terrain, _, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,B,C,D,_,F) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, region, axis, terrain, _, count) = group - new_grouping[(region, axis, terrain, count)] = ([], []) - - for group in grouping.keys(): - (_, region, axis, terrain, _, count) = group - (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, count)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - if world.mode[player] == 'standard': - # tuple goes to (A,B,C,D,_,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (std, region, axis, terrain, _, _) = group - new_grouping[(std, region, axis, terrain)] = ([], []) - - for group in grouping.keys(): - (std, region, axis, terrain, _, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(std, region, axis, terrain)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - else: - # tuple goes to (_,B,C,D,_,_) - for grouping in (groups,): - new_grouping = {} - - for group in grouping.keys(): - (_, region, axis, terrain, _, _) = group - new_grouping[(region, axis, terrain)] = ([], []) - - for group in grouping.keys(): - (_, region, axis, terrain, _, _) = group - (forward_edges, back_edges) = grouping[group] - forward_edges = [[i] for l in forward_edges for i in l] - back_edges = [[i] for l in back_edges for i in l] - - (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain)] - exist_forward_edges.extend(forward_edges) - exist_back_edges.extend(back_edges) - new_grouping[(region, axis, terrain)] = (exist_forward_edges, exist_back_edges) - - return list(new_grouping.values()) - elif world.owShuffle[player] == 'parallel' and world.owCrossed[player]: if world.owKeepSimilar[player]: if world.mode[player] == 'standard': - # tuple goes to (A,_,C,D,E,F) + # tuple goes to (A,B,C,D,_,F) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (std, _, axis, terrain, parallel, count) = group - new_grouping[(std, axis, terrain, parallel, count)] = ([], []) + (std, region, axis, terrain, _, count) = group + new_grouping[(std, region, axis, terrain, count)] = ([], []) for group in grouping.keys(): - (std, _, axis, terrain, parallel, count) = group + (std, region, axis, terrain, _, count) = group (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel, count)] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain, count)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(std, axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) + new_grouping[(std, region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: - # tuple goes to (_,_,C,D,E,F) + # tuple goes to (_,B,C,D,_,F) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (_, _, axis, terrain, parallel, count) = group - new_grouping[(axis, terrain, parallel, count)] = ([], []) + (_, region, axis, terrain, _, count) = group + new_grouping[(region, axis, terrain, count)] = ([], []) for group in grouping.keys(): - (_, _, axis, terrain, parallel, count) = group + (_, region, axis, terrain, _, count) = group (forward_edges, back_edges) = grouping[group] - (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel, count)] + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain, count)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(axis, terrain, parallel, count)] = (exist_forward_edges, exist_back_edges) + new_grouping[(region, axis, terrain, count)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: if world.mode[player] == 'standard': - # tuple goes to (A,_,C,D,E,_) + # tuple goes to (A,B,C,D,_,_) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (std, _, axis, terrain, parallel, _) = group - new_grouping[(std, axis, terrain, parallel)] = ([], []) + (std, region, axis, terrain, _, _) = group + new_grouping[(std, region, axis, terrain)] = ([], []) for group in grouping.keys(): - (std, _, axis, terrain, parallel, _) = group + (std, region, axis, terrain, _, _) = group (forward_edges, back_edges) = grouping[group] forward_edges = [[i] for l in forward_edges for i in l] back_edges = [[i] for l in back_edges for i in l] - (exist_forward_edges, exist_back_edges) = new_grouping[(std, axis, terrain, parallel)] + (exist_forward_edges, exist_back_edges) = new_grouping[(std, region, axis, terrain)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(std, axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + new_grouping[(std, region, axis, terrain)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) else: - # tuple goes to (_,_,C,D,E,_) + # tuple goes to (_,B,C,D,_,_) for grouping in (groups,): new_grouping = {} for group in grouping.keys(): - (_, _, axis, terrain, parallel, _) = group - new_grouping[(axis, terrain, parallel)] = ([], []) + (_, region, axis, terrain, _, _) = group + new_grouping[(region, axis, terrain)] = ([], []) for group in grouping.keys(): - (_, _, axis, terrain, parallel, _) = group + (_, region, axis, terrain, _, _) = group (forward_edges, back_edges) = grouping[group] forward_edges = [[i] for l in forward_edges for i in l] back_edges = [[i] for l in back_edges for i in l] - (exist_forward_edges, exist_back_edges) = new_grouping[(axis, terrain, parallel)] + (exist_forward_edges, exist_back_edges) = new_grouping[(region, axis, terrain)] exist_forward_edges.extend(forward_edges) exist_back_edges.extend(back_edges) - new_grouping[(axis, terrain, parallel)] = (exist_forward_edges, exist_back_edges) + new_grouping[(region, axis, terrain)] = (exist_forward_edges, exist_back_edges) return list(new_grouping.values()) - elif world.owShuffle[player] == 'parallel' or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player]): + elif world.owShuffle[player] == 'parallel': if world.owKeepSimilar[player]: if world.mode[player] == 'standard': # tuple stays (A,B,C,D,E,F) diff --git a/Plando.py b/Plando.py index c1cd8c23..fa9fa848 100755 --- a/Plando.py +++ b/Plando.py @@ -162,8 +162,7 @@ def prefill_world(world, plando, text_patches): world.owShuffle = {1: modestr.strip()} elif line.startswith('!owCrossed'): _, modestr = line.split(':', 1) - modestr = modestr.strip().lower() - world.owCrossed = {1: True if modestr in ('true', 'yes', 'on', 'enabled') else False} + world.owCrossed = {1: modestr.strip()} elif line.startswith('!owKeepSimilar'): _, modestr = line.split(':', 1) modestr = modestr.strip().lower() diff --git a/Plandomizer_Template.txt b/Plandomizer_Template.txt index dce3f005..41782f89 100644 --- a/Plandomizer_Template.txt +++ b/Plandomizer_Template.txt @@ -246,6 +246,6 @@ Ganon: Triforce # set Overworld connections (lines starting with $, separate edges with =) !owShuffle: parallel #!owMixed: true # Mixed OW not supported yet -!owCrossed: true +!owCrossed: none !owKeepSimilar: true $Links House NE = Kakariko Village SE diff --git a/Rom.py b/Rom.py index 5dc3e361..47bf48c1 100644 --- a/Rom.py +++ b/Rom.py @@ -646,16 +646,16 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): # patch overworld edges inverted_buffer = [0] * 0x82 - if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] or world.owMixed[player]: + if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player]: owMode = 0 if world.owShuffle[player] == 'parallel': owMode = 1 elif world.owShuffle[player] == 'full': owMode = 2 - if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player]): + if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] in ['limited', 'chaos']): owMode |= 0x100 - if world.owCrossed[player]: + if world.owCrossed[player] != 'none': owMode |= 0x200 world.fix_fake_world[player] = True if world.owMixed[player]: diff --git a/Rules.py b/Rules.py index c3842593..abc447ba 100644 --- a/Rules.py +++ b/Rules.py @@ -2053,7 +2053,7 @@ def set_inverted_big_bomb_rules(world, player): else: raise Exception('No logic found for routing from %s to the pyramid.' % bombshop_entrance.name) - if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player]: + if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none': add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: False) #temp disable progression until routing to Pyramid get be guaranteed diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a1d73102..edbb11f0 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -117,8 +117,13 @@ ] }, "ow_crossed": { - "action": "store_true", - "type": "bool" + "choices": [ + "none", + "polar", + "grouped", + "limited", + "chaos" + ] }, "ow_keepsimilar": { "action": "store_true", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 3968360d..c4ff9781 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -202,7 +202,15 @@ " will have an independent map shape." ], "ow_crossed": [ - "This allows cross-world connections to occur on the overworld." ], + "This allows cross-world connections to occur on the overworld.", + "None: No transitions are cross-world connections.", + "Polar: Only used when Mixed is enabled. This retains original", + " connections even when overworld tiles are swapped.", + "Limited: Exactly nine transitions are randomly chosen as", + " cross-world connections (to emulate the nine portals).", + "Chaos: Every transition has a 50/50 chance to become a", + " crossworld connection." + ], "ow_keepsimilar": [ "This keeps similar edge transitions together. ie. the two west edges on", "Potion Shop will be paired with another similar pair." ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 7612c1bb..12e8bef8 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -118,6 +118,11 @@ "randomizer.overworld.overworldshuffle.full": "Full", "randomizer.overworld.crossed": "Crossed", + "randomizer.overworld.crossed.none": "None", + "randomizer.overworld.crossed.polar": "Polar", + "randomizer.overworld.crossed.grouped": "Grouped", + "randomizer.overworld.crossed.limited": "Limited", + "randomizer.overworld.crossed.chaos": "Chaos", "randomizer.overworld.keepsimilar": "Keep Similar Edges Together", "randomizer.overworld.mixed": "Mixed", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index 15b89cab..2d15fdd3 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -21,8 +21,15 @@ }, "rightOverworldFrame": { "crossed": { - "type": "checkbox", - "default": false + "type": "selectbox", + "default": "vanilla", + "options": [ + "none", + "polar", + "grouped", + "limited", + "chaos" + ] }, "mixed": { "type": "checkbox", From 16e004060f66b7cead6a290b7f86d9cf7cb6de6e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 24 Aug 2021 04:53:55 -0500 Subject: [PATCH 084/104] Fixed issue with Grouped/Mixed+None/Chaos Crossed shuffles --- OverworldShuffle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index da55a883..5ae6fdbb 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -93,7 +93,7 @@ def link_overworld(world, player): raise NotImplementedError('Cannot move one side of a non-parallel connection') else: raise NotImplementedError('Invalid OW Edge swap scenario') - groups = new_groups + return new_groups tile_groups = reorganize_tile_groups(world, player) trimmed_groups = copy.deepcopy(OWEdgeGroups) @@ -132,7 +132,7 @@ def link_overworld(world, player): swapped_edges = shuffle_tiles(world, tile_groups, world.owswaps[player], player) # move swapped regions/edges to other world - performSwap(trimmed_groups, swapped_edges) + trimmed_groups = performSwap(trimmed_groups, swapped_edges) assert len(swapped_edges) == 0, 'Not all edges were swapped successfully: ' + ', '.join(swapped_edges ) update_world_regions(world, player) @@ -148,7 +148,7 @@ def link_overworld(world, player): # crossed shuffle logging.getLogger('').debug('Crossing overworld edges') - if world.owCrossed[player] in ['grouped', 'limited']: + if world.owCrossed[player] in ['grouped', 'limited', 'chaos']: if world.owCrossed[player] == 'grouped': crossed_edges = shuffle_tiles(world, tile_groups, [[],[],[]], player) elif world.owCrossed[player] in ['limited', 'chaos']: @@ -182,7 +182,7 @@ def link_overworld(world, player): elif edge in parallel_links.inverse: crossed_edges.append(parallel_links.inverse[edge][0]) - performSwap(trimmed_groups, crossed_edges) + trimmed_groups = performSwap(trimmed_groups, crossed_edges) assert len(crossed_edges) == 0, 'Not all edges were crossed successfully: ' + ', '.join(crossed_edges) # layout shuffle From 0f8efed663b35928cc9ac0239d18784018573768 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 24 Aug 2021 23:26:57 -0500 Subject: [PATCH 085/104] Fixed Limited OW to abide by Keep Similar option --- OverworldShuffle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 5ae6fdbb..d705f1a1 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -169,8 +169,7 @@ def link_overworld(world, player): if world.owCrossed[player] == 'chaos' and random.randint(0, 1): crossed_edges.append(edge) elif world.owCrossed[player] == 'limited': - crossed_candidates.append(forward_set) - break + crossed_candidates.append(edge) if world.owCrossed[player] == 'limited': random.shuffle(crossed_candidates) for edge_set in crossed_candidates[:9]: From 151506d130cd66f022d0b1beb48c5169090b11e3 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 25 Aug 2021 02:22:21 -0500 Subject: [PATCH 086/104] Play SFX on crossworld transition in Crossed OW --- Rom.py | 2 +- asm/owrando.asm | 6 ++++-- data/base2current.bps | Bin 141164 -> 141181 bytes 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Rom.py b/Rom.py index 47bf48c1..d27a0f04 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '261aa02eb4ee7e56e626361f170de5f4' +RANDOMIZERBASEHASH = 'df12af8b5970ecc392b29b45595d9351' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 07173255..2ad5e8cc 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -342,10 +342,12 @@ OWNewDestination: ; crossed OW shuffle LDA.l OWMode+1 : AND.b #!FLAG_OW_CROSSED : beq .return - ldx $05 : lda.l OWTileWorldAssoc,x : sta.l $7ef3ca ; change world + ldx $05 : lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq + + sta.l $7ef3ca ; change world + lda #$38 : sta $012f ; play sfx - #$3b is an alternative ; toggle bunny mode - lda $7ef357 : bne .nobunny + + lda $7ef357 : bne .nobunny lda.l InvertedMode : bne .inverted lda $7ef3ca : and.b #$40 : bra + .inverted lda $7ef3ca : and.b #$40 : eor #$40 diff --git a/data/base2current.bps b/data/base2current.bps index 512a6dc9d18d1f1cfd322a5900c2cf095832b4e8..10f67fd0a1e0181683d24b54a8b6ce7779b974b7 100644 GIT binary patch delta 149 zcmV;G0BZm2&ItX^2(U>31l%bj)`Lv}w@m>7@B{%-mp%poAXinU1-}5gs?W;ve((v8 z%JY7yIE^m>k|na4Ew4&|Fwh8>5JdKYfSVu@rmaap$w1H(rCrGc@C2(%c&43z4W(Vl z7tjKm3xJuW1%-;jFkqu2O7sKZJh!9<0nShYc{P`GR{=8tSGTBF0ld!$5HG=y*vne_ Du>U|u delta 132 zcmV-~0DJ%a&Is(z2(U>31c)zp(1T3@w@m>7@B{$`mp%poAVx~21)pML^^zsBfi064 zhk*!}D(8WKn+g%8tw})1K+qGVUC9LS1gkZ8rk#KdrCrGv&;pwafSIKQg^I#3V51{S m^e^Bhx3vZV&QJmmHkXB00W$$Xx4BmVyw3=kE9FaYHnYmQ!!^JF From 0b05ec9927d7261e498fec9898fdd079fedf6bb0 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 25 Aug 2021 02:22:59 -0500 Subject: [PATCH 087/104] Improved accuracy of Crossed OW flag in ROM --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index d27a0f04..15857882 100644 --- a/Rom.py +++ b/Rom.py @@ -655,7 +655,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] in ['limited', 'chaos']): owMode |= 0x100 - if world.owCrossed[player] != 'none': + if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): owMode |= 0x200 world.fix_fake_world[player] = True if world.owMixed[player]: From a7554530613d8005194e0184b15f50531f6daca1 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 26 Aug 2021 00:55:37 -0500 Subject: [PATCH 088/104] Updated baserom - Fixes TR Pegs issue when fluting direct to TR Pegs from area with hammerpegs - Changed Crossed OW to only affect Link state on world change --- Rom.py | 2 +- asm/owrando.asm | 32 ++++++++++++++++---------------- data/base2current.bps | Bin 141181 -> 141185 bytes 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Rom.py b/Rom.py index 15857882..c9ebb8ad 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'df12af8b5970ecc392b29b45595d9351' +RANDOMIZERBASEHASH = '712ebda1ef6818c59cde0371b5a4e4a9' class JsonRom(object): diff --git a/asm/owrando.asm b/asm/owrando.asm index 2ad5e8cc..8082d38f 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -342,26 +342,26 @@ OWNewDestination: ; crossed OW shuffle LDA.l OWMode+1 : AND.b #!FLAG_OW_CROSSED : beq .return - ldx $05 : lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq + + ldx $05 : lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return sta.l $7ef3ca ; change world lda #$38 : sta $012f ; play sfx - #$3b is an alternative - ; toggle bunny mode - + lda $7ef357 : bne .nobunny - lda.l InvertedMode : bne .inverted - lda $7ef3ca : and.b #$40 : bra + - .inverted lda $7ef3ca : and.b #$40 : eor #$40 - + cmp #$40 : bne .nobunny - ; turn into bunny - lda $5d : cmp #$04 : beq + ; if swimming, continue - lda #$17 : sta $5d - + lda #$01 : sta $02e0 : sta $56 - bra .return + ; toggle bunny mode + + lda $7ef357 : bne .nobunny + lda.l InvertedMode : bne .inverted + lda $7ef3ca : and.b #$40 : bra + + .inverted lda $7ef3ca : and.b #$40 : eor #$40 + + cmp #$40 : bne .nobunny + ; turn into bunny + lda $5d : cmp #$04 : beq + ; if swimming, continue + lda #$17 : sta $5d + + lda #$01 : sta $02e0 : sta $56 + bra .return - .nobunny - lda $5d : cmp #$17 : bne + ; retain current state unless bunny - stz $5d - + stz $02e0 : stz $56 + .nobunny + lda $5d : cmp #$17 : bne + ; retain current state unless bunny + stz $5d + + stz $02e0 : stz $56 .return lda $05 : sta $8a diff --git a/data/base2current.bps b/data/base2current.bps index 10f67fd0a1e0181683d24b54a8b6ce7779b974b7..997df3d6bc406f60caf48ec8383651712d25f0be 100644 GIT binary patch delta 5560 zcmW+(30xCL7tiE?a3`RM0vgr>5X6Xh;f0Dqm1kYd|z(6)33~|K; zv%o|Qh%1eX;DLgz1g%8tr+Cz>myNc@qxEe4h9T7N*!#waOCMP(EjRc5lfkQ!zAgg6A?d z+E9pwP)5Sj#5q4Q&_peqB$^&)EN9{-7Q9XIAQN^QiSq@>DcfZvChV$rsNHgEdrT9j;5VXpKSL*atRb@APa(zmT4EG@E1C=>aDq5CU`H9ggtFU2 z+_hqmL1_sM%oRt2L$ENu(#G}9|7%Ca;URQ>08;PYbNiq}6u+fr@K>FKxB*52* zfFABEK^k1|p6$2r7D_Clve}P>l)|8$41aU~R9;=Y$-=hWx1xz$O!cUJU6@yWtG!JdM7$a69w=` z=_vUkBO1?X3HQJ3$N(2kPBoisth(;j2||G?Hy*=Z!MVLnNO-N6MVo=Lz3KlIE}Km8wa9xxI|$~ouH zM&et2xN}eS)V-X_fyHx53u#sGZIrRoMDV|GPUk@uo9nM%xe(H*nt2U5Gq8ewzJxlp zj$*FodRh!c6S7V&!=@o7vxBlsi>V6LCSC94`xMax2NhoZBfls?&cn>sVP!g+@GL>w z!|<9<4XBbPwi-|xG7|GB&e>^H8Q^DLso*v|;uY=VfQ!_m##y?tz@!=h|L|Hi%yb29 z*V4q*My^*24fft3l1f=oB%MQ1QHLvtb!CWvaGiJH$bDtV8=s+SLw$rJcj| z+Gc7QdaNauRn^KDY2!%VXKra9vWjqV{~39YmgrZ_3c}bPqiRYuavoCo8Sw7l@y0z#(`>LqHS?N zOaxd7izX(Go>h#t>$GkRrzqCoFpoLkslELxmy%Db4i_VD4RwN9HSfnn(&Tg!9mQw} zzU+@SFeye9Kz@%54&yq9JJH4z8`oq0uWkBN0Km7l#DvepA)_uJE1-WD@{m%xB*c{O zks5iAS1W?bi5OdZ;x_<<+rFA*6alHteYQ~ILtR9qCvgV1V&c6maNam~aM3n*;Sq1J z!X{dgCJJ=gQR6vrcNnR3u`85?O?G!zF!2tqUDXe~vuRdIy+Ex^|HUB*@U~6fv;hEz z?eOM5B;Y>Wm%9SIv_&9KxFCD(z08np{2(`x8}wdXrw)&_1L&CrLtTD?7!o z8-0jR3AKcPJQ#0LB@eoQi1;GVF5AzwE!l-0$cV}!L97Oi)}VF`N-ae93eo*S^q>&! zEJ6>9keZ)HD(%1p)a}X|Re!5P%1lmqcYYSUV@Y=NP)f;h z;`WESH2)z<`ZCpG4g4gBrn(G1B;yY!9GDEoz)c5c2NrZ8ORy{4oZ(6TR9v-=QfgA( zsA}Q$1D{PAom{b&yylP*-a2&H^Azc0mzRpH0<|U4PO5M6Q%I#}{ktID8+EiUMyJ#* z)GgPo(5=y}({0p^hhJI4f@5Y^R5*wZ9m?R@2@1p04Dw9xQ)z1o0h_D=IE{a6ordLb zufO0uTyJ@%RXBXeSj9qkr+%e$o&mX@XE1Z&oQCCK8?0|A2nrnhH%gyss&?f2?5w`9{s}#Xb?nZ}=!LQdHmO4__br1jNE6 zjd5T!)HkNBa!V>QM!r?^dN`)6LS|X!5?hwkg_OZv7Im^^mP1SgbRx!PbxHa3=eZ7H zmO5)+$~YFT7wXyNB7KYZf8A~oZ>$1=I^qqJs&Z0lb^~rbU_Ojorqd6YOqwUboxo*4GSfdhM&u>lu+>VS8!Dh ztrfc5m@zX9Fn)$X3tZt$7{;DwsNg!^wdCFn0ea2)o#yRQ8b(gLyOvTF!ZCb2_y*?i zAw#oh#0($U&CCrVVNgCLHK&A-WLA(d^;r@p|n1rvoyYslBs7!Ad6hu7`<4qwHekZc+c zHp7;t1$h;fyk3+OnILct;uA1Ls<5$y*#^B~`RQ?_g%{mS1ws^DlZdgEVH&2sN# zd^#ic%BRxI-Uak!mfH?vNtj781j$S+W)q65YAWz7IlqtrnhGl(_vaTeBX&|7%}UbP z!g3)2j7L7z#41amrCQkQA7^K>oT9D_BHRIbJM1hsr1x3FZ)I+sl2{pEQe-Xir?UbE z(4PmupP_pIZFUQwjl*$AG*b*`&eNIuz~e{aB&)OUngij}BYH3rl1Kl>Z{AJE=F6t` z&4*L^=4%$p*ufZj!W+k4MF)TBNmc-XL(LbFW?6_Q`Qs4Gd9bQ@HX()SCnCTJn0sPS+=*IqkUh8M zRIPacW^J|R#h9I~H7~X2Ze>0u$}FAt`j^@tC=_FBnc;J)-)<>_a(MYffP8F#p0b-q z1n6Nvlcv(GZaDnw#Al!{Tzqm(%5DpO*(8Yf4IF*H`lQ$z?@JtP59Ze-7_>E$BpnsT z(-Lc`*j&zxTv)A5xoY{2ty_~`U3(B(sFSvz-N{tX8tqL>G4zfinXjSjR2Y~CV^1mP zZNwRDRID4RA^Y7-747cu#n>X@i=O4GtqOs<&MUvdm2^{DGN*w_N-$^~^*A!%XAro{ofc~X_AgH zR*RxJz2OH;P0ei*lDf$*C#m~*kAIp2j`Kd0=4ne?25HVFX`lYB!L5cp#y+j-4Frs* znj=Z0t~G-fF_Udyw;cyyxozaxW1vrH5Pj6k3VJ% z2?bdYc5MLR(Cxx(umPrCSmQBfic6cpFa06W_T7awLa-i=zBCm0z$KSvNIyHt2ffHu zBtvv*ZusgHx{A=AMh_JHdN@==5$2EuwB}wZKP^ztjt$gPtITc`TnYPk$ALF+Zufvb zISV{VYI^%j{Nm|i7Q~V!QOaYuy*niE(;O+;^7vGn-@iVQ@1GcEozHEPGa2wu_n%-b zta9Y}uTtpY99=aPW?f0=hgtL3C>V2j*rKtChKh!)8iAl#&8(bur)BpUbm$EF{tUWw z7X5q{9Sg72;u?$DcT}zTqdl`T)7~2~4llTtG6`fkU9#8p!3OKc9hSc%WiFAzdXCvL zzUi{!7;LyaI3Z&2Z&hMffW;n^?a@(7Nezwc%r76+C4Tv+EPS(ZE%o7kdt3ckB#W}U z9O7T)10N^8IK<5Ktb6B8IKG?xxiv) z=*CXi^n(cufJs+}jC9}fv}~fPB$X8H7qr=G(SA{tn+(IF|3PUlZ2k}W_QDpp_i6|j zU~9h03&B@#^>syH#8^Ewg6Z#DYMJpO*WtmBc##{=B*3ccp^+jlP4v8dnMvCzm$%X%Djqa2i}+|01{U zPZ;`zL1S1~yBMb37#CR6$;>aL`Y?-9f3Nc4C-z=sA~OtD-x%(LhX%)&?jAX(@kUAw z?71=4U*K=F4119qw0;~e<@{hFbcc$YMMCi$XuYWfJ@Dqu^1P{Q+{yo`zIRD*+OWD; zedMQDc8U1y#Xtpn$vkQFALeNv-ZgI1NUfJnuZwcQNnhOBFIRR@uWD2JRejr)AQ1Aa z@%M;tZ&hoT;EF3M&RF(Af3|uz2Z-%tO{uSWVU)pCDkfe)&Ne9fWnXfs^q?S($ZX^y zwyG^0{NvXAe)GEz6etymxE-awSsawD5Y%%5MH*aqdyHpdchJfQng?pdd}z9z4C>*X z+j$@WF8?veJNoh=q2k_x5iU{6R+#@|Di{c_{#Z3oa@qQ}T~;ZWZjMyQ$)?JLu*QTi z$6G5!3#K#6q2W$o=(IQwvTiG*`8yP+Tf zj=g&sT!GSiYk?JheQyUyg)i=<13R33KNRDv`!oBc$9Rxd0SSr?g0wun7hp2NJV+P{ z&)p9TPT5l0cz0@#(&g5MTk<)(PIuW54Ft)={jvuq`>L-ams^+YWii=lCF!uRb|oW9 zgO?u!;1A4S9@O@W7+0%EFXw z%P;d=ql3YVjK&>H5B3P*&DybW=C3>aw#|4)Wo_DAIAH11!C~4pw#&Z`@bnCuxl8Lu z02cIzf4kJc4}SJE6@Qi;dAbyggMQEY_5Z@7q($iR<`>|y-^~gPm@_mbWLChb&(4Ca z5Ii3QvTb3{8Icqh@`@iIlMT=O-8Xt&qP9W+9O3+w=M9<6-IX6}X+NuEh`HRr)BA%R zG0ewClbZu%@N3z02wu{-3NXCvi_^U2)jwx9j|RXEfEXrT2!{5Lv7eiqKJl71VkY!trigRrbto212I)R3yoJ>>n-d?0|v7RVTdb6 z%mRZL5Eo4qMHEC^3APezKM!g>Xg_PC*0kEH*iXNq$?whk?;LOD&Aj(!9@Q=W-&Jub zF*eAAcxwtCQSFZ?ltj|1G_q=cg{t{Fay7j9=9p^QV|dE?nDo}0Y3sUpa^aSqaq0h5|d`)gb9m@${=CgOfXQ6)M*_$?SAE-6P7eiIXkun#O8)ZITZ{#~MpW*T|p3G2$);k(VmP^jh) z1HI754;~`=9$hl~xeJ^gu4$BxuSFXx_ZI6i43R5$(Zn)%kv}gYyA%BY13GnW$5<6Z z6K6SgrB*ixqT!T@keFH593xGzFi9{a-cZiOC+>ce?nS2TGZB||BbRKCiTGeoy-TGt z6Jfg%Ins~3%mlp^7g6V-`p^@aXn`jL34Z!c^h8VKy-OyA8XXY}-v}lGKKwv9Heg#B zRzlfrChpiU$fb0I1{MfMfd<$hoCLPQ$HFn71%`=+d7?6O$3!fIGey%uik%j11ma&V zBOX>G0(yC_0(0Sd&wRhEAJMd8YPDq!pHdig3Gi3XW%5INQ7U65KJMQy89v0PaFKX~ zSk;A8c+YmhVlfdf>R*P-weqOW)yVFriF4)bBNMTy9J!R`TBC{K8+f?y@G_KFP7|4( z=qOE4@F(#Y`8*Sv!0HHL>Dj0;n$iklKOE|n9`HjKI>>x7zbG9KirrJcpH_Vh3%o`J zPQokEG&@%sjFoiA&b>{YG_ez&_j=FYg-<_ymx;KJd6EE|!&4}d%mh#2N0L1C@Bcxr zBPQaja@O^WiTK(O>Dpg4=>V(x4lmCoE}~T-cTnywGr|3rb-9jO@Nxb1kR42#R8wvs zS1#V52Uk(2&RN3Vx;ZbFq6t|in`_q+36`OG=4`4$wNc-%d|)w6R6>PxVD!4Z$koJb z8&RgG3D3P~M>rm{#E1^k#AYL!hfKt5igk6GRC>5vIv3oAUr9&#d@~i10Q?1K$zliXF`^WTBC&DZ z=h)m!%_Fr!k`iJUPKQvo805oevWuf;GajGr5@^%X4$!^I>UZ=%LD=0UO~9f0Y=! zVeXMCVuC$8AT|mthsB9A$4xCkJM=mahE3Dg)w6$^ zMw(qNqP+wS$IABDK~k(BVCQ$p=rpZ!dJ?TniD^CNZ|qYh0RTGe(^8fT2ghDPHbDQz z=OCqYi-^SUkQ#YUP%A>pi5Ppwv=aafw|_aqBmg42XBuDRQ}8V!CB!*g3WW~pT4LE~y$v1qv^6)GG7Q!8M zO0W+$)w1K9&j6Nje4JRI%2;$)OBWb}@7pe_)M_O|v^v@-g z>nNo*y-&r$ACD|g8l76Pmb~GV5ng)KBsoX=IOL@Q8&6$3%|WVrxMWf(ssATP-=~h& z$LL4vv-FGgx%zy)TK}2;J-F2t9v78XQQ;)o^(dFKj8ho@$R*qRze-!uhth5#v8Y?j zEx}6hoI=&btIQ_ouuaCRlGb1Gjx^N%sZ%(8$gzqn*jv9!oNh$!=ebNOoZYYld<*C)`x{D!=%H|ko=Syh&B8nzQ0Fn>Fzu*`2$_4cY`_XiSGb9?1dKI)K=!G*1K2vk zxzFbmMhoiO{NVqNE(N3D$Bpq|G%RbJ2YB$;M#XelCt~b2x0sv%yuiuNQ|BE@A8&#A ze1j!hU^wIbZ?#8MpN+>;NA)qYR4z(wS%XV2sDZ;4>kWe@lGgWO`mwaI>(xl*up(lt z1`$_aB$0R)5j?Vus4yU+7ut^{r%FFHXt|{g3q+Kb%do5xQSj4OFn0s3xM?novSV@W#|TIai#Jn(sdLA!pJb(5He!zSNbOR09j54eOO97MKvM)!+?MhS3A zIC*=4Q%FemvBXPEtDuPslxNb1a6kzdEoR_JchfEPXM38 zbH}r=vC2;b=G{HO-jOLrn#ri@->pATVRd!X4eNf&%TjKhYUcEU4UE)-KI4>Ddsx`} z|8^Y1=^1|!O-rla90T-jQ=@7-irE%DGt>EwHeUM}?w*4m2e-|w$iTPIR|wpIgX`-bHP+DoN8R3p+S~@zPMoEXuvG zwu;YlORLYzEUcog3?f_s`YXPHd%jWxfdJad3!qIS0|wEfm=ZW?w%+Os&wUjyT9$v$ zDuaK2WdPAod-89v8tyoiDf=Ku11AJ&vIMKLi38ab`ML2GwU`Kr&>`OdS2# ztgh{J3T;AiutR`3j|_yN_6k1s%0qJsEyHFSD|y<^{yxDMr$Ny2MM!7W;|6u3zf{Jz zNgdv$d>fDUm#VD`p}u7?$b^qtR{BO18nmsh1BI}%B#jWkoU>7drxCMd^y`nv`(){J zmRHT|IolSbQT9a-gZJ{#?>&xXH~t~S`4KD2AKM33C#Mmq#~CN4yOl%Gs*x1<*-KLmVo2hgC2FmryD}a z(yCktS{DO9_;Kr+bXbcor4-?zA>$rYwF+$szQoavP;PaKQCFQLx>#X4FS3;it>sMg zf+}75wc2kib*txB)f|PjRIB|LPjZ%|TGvxs0=;8MW&;d57Y;szLP zB?t7(73{kx#<*6*75`bFwkde(I;p0@J+nu=cUA*4ImM`Tj%Nn{m0vX6hQ216!rDgk z^U|QUD9ja2K5Zl6sdF#+J3JtFKE5!szM(_8Z~F)Ef;zLI|DT1)imBo#GeKK-ka}dX zQqLNUv~T0^M$W7a;J6AZ&i;MlEH1Ob$6k6evw=Bj{mXn6gPIe@SK4W(7+Y;vb7q4R z)3N3@5lP*&EFr18SVmv7>@?>?X@5V{mP=amNm??nC8Wi;-z0CzY#0g?sOD(Wq-)9L z1k8B*_O{aiWZUB}oC1LZLgpM1e7rDBwbwO#9o8l|m41UvYVyfWPe%xWDUj z5DukR(!d(H;K~}WxMa63m&>`HY`=144IfudxEcW@aLLuF;-#%z$cqBSbZEPp5xFd# zt|W@iqh1BK9!6*>!Wx`KYwwqGiNOYoGT4A)w+986!Xe%9aWBs!cW?=_b4cHr=oFq) zWLpzWN^DD`NfGmDmV~6HbWF!rSQnEumNW~}e}h!_;NZm@#N?UZ&b9fy^@)D>%sAt6 zL7SY(f%fj-!6&fVS?HguFu+;*Dk|Kziq?eN3M~xARi6n5BjZCDC1?AJ=6JD}DPlJ9k~h7ubaEt9@@T$lXCDiGd8w- zNjm2gc6G^K;T}i6zwHq>&3M74fyLLu{A(|wko?`bkBxjeuhzlPjbFo4*UcahrvETJ z4s89SEK#+0E-5(7Yip`OhXs`$G7L}r3&s8LtAC+yKRgAkKMV$e_SPRbK3EUe+*HI9 z?;EJmOn`4`?bH_qPA_isi-H6u!MF55+t7=EoJma7@i+~ojOM!)jlRIK-8s-Q4z}M+ z@*VVk>DRMf6bv%rg0Mo#tx-{l`GyC@JZE534m+Ia{Fdc-WRz2&h;wWsK~!B*zQL#w zk!nR8TyiVcfAK2AMSHDdxYHx&NPicL5FWZUShQlMfk}pKx5fy}UO)NWY zQRW*=LsNUWNXzzYD7ii8z4$D{1^zVcAOJVZF(#F7!@LC>@ zx9f3p-S2R~XGX1YT}>ujbbEZTwUfy#q5_$@bAPM!;S&3GNMsc7@a>U4xG5~5bYI-8 z#@p#u_|xt8{kO?YwIf~>gsdNrAANqX2m%<%)73OK}W zIX6h7bc(r#Ifah5PCKOcMpXFmwTc*CcQI=ib4N+=5anipic&!d#J&T69@M!9jX$in}3(hMT?ip5eQ_xh$M-c-VjubB3n)%*Sx* zpBHd)E_yx`tguHsX9S~h2BEkPne}mdZDO8V%?-oB{0Z}N`5Y3z$~R8aRq%i_l1qNx zu#&l_hQIwC3T8vmOM1YPE#w7sx!|1At*|l1!<23E{ElDhbUg%sfum6T9!5l84r8+>!F_E zIg)Q_Umnl%taSFbSGI0H#R&O(W>VXxc#&=6w(CR5>5Lx$gvZh-gZh^h&Eo;^03e1* z=7WfVF%Hz1r*?4eKODo|Py4L03D0zS32J5wvx5)ziZ*(=8F90>0J!+lv(0jPmr zys}=a;O$$mSgQ?}UmQ|rP6Q#~J!W4Phy-(+&u4)*UDD;Nf220o?4owmh?*}A;Xmht bk Date: Thu, 26 Aug 2021 06:00:00 -0500 Subject: [PATCH 089/104] Updated OW GUI layout --- resources/app/gui/lang/en.json | 2 +- .../app/gui/randomize/overworld/widgets.json | 22 +++++++++---------- source/gui/randomize/overworld.py | 18 ++++++++++----- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 12e8bef8..8544efff 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -124,7 +124,7 @@ "randomizer.overworld.crossed.limited": "Limited", "randomizer.overworld.crossed.chaos": "Chaos", "randomizer.overworld.keepsimilar": "Keep Similar Edges Together", - "randomizer.overworld.mixed": "Mixed", + "randomizer.overworld.mixed": "Tile Swap (Mixed)", "randomizer.overworld.overworldflute": "Flute Shuffle", "randomizer.overworld.overworldflute.vanilla": "Vanilla", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index 2d15fdd3..faf4a100 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -9,17 +9,6 @@ "full" ] }, - "overworldflute": { - "type": "selectbox", - "default": "vanilla", - "options": [ - "vanilla", - "balanced", - "random" - ] - } - }, - "rightOverworldFrame": { "crossed": { "type": "selectbox", "default": "vanilla", @@ -35,6 +24,17 @@ "type": "checkbox", "default": true }, + "overworldflute": { + "type": "selectbox", + "default": "vanilla", + "options": [ + "vanilla", + "balanced", + "random" + ] + } + }, + "rightOverworldFrame": { "keepsimilar": { "type": "checkbox", "default": true diff --git a/source/gui/randomize/overworld.py b/source/gui/randomize/overworld.py index 50cad240..77be948a 100644 --- a/source/gui/randomize/overworld.py +++ b/source/gui/randomize/overworld.py @@ -18,8 +18,9 @@ def overworld_page(parent): # These get split left & right self.frames["leftOverworldFrame"] = Frame(self) self.frames["rightOverworldFrame"] = Frame(self) - self.frames["leftOverworldFrame"].pack(side=LEFT, anchor=NW) - self.frames["rightOverworldFrame"].pack(anchor=NW) + + self.frames["leftOverworldFrame"].pack(side=LEFT, anchor=NW, fill=Y) + self.frames["rightOverworldFrame"].pack(anchor=NW, fill=Y) with open(os.path.join("resources","app","gui","randomize","overworld","widgets.json")) as overworldWidgets: myDict = json.load(overworldWidgets) @@ -27,9 +28,14 @@ def overworld_page(parent): dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename]) for key in dictWidgets: self.widgets[key] = dictWidgets[key] - if framename == "rightOverworldFrame": - self.widgets[key].pack(side=LEFT) - else: - self.widgets[key].pack(anchor=E) + packAttrs = {"anchor":E} + if key == "keepsimilar": + packAttrs = {"side":LEFT, "pady":(18,0)} + elif key == "overworldflute": + packAttrs["pady"] = (20,0) + elif key == "mixed": + packAttrs = {"anchor":W, "padx":(79,0)} + + self.widgets[key].pack(packAttrs) return self From af96bce77bfc4df0fc142be403cb6f82c0bdaa99 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Thu, 26 Aug 2021 08:02:06 -0500 Subject: [PATCH 090/104] Version bump 0.1.9.0 --- CHANGELOG.md | 8 ++++++- OverworldShuffle.py | 2 +- README.md | 53 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c7b2c6..00f42918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 0.1.9.0 +- Expanded Crossed OW to four separate options, see Readme for details +- Crossed OW will now play a short SFX when changing worlds +- Improved Link/Bunny state in Crossed OW +- Fixed issue with TR Pegs when fluting directly from an area with hammerpegs +- Updated OW GUI layout + ### 0.1.8.2 - Fixed issue with game crashing on using Flute - Fixed issues with Link/Bunny state in Crossed OW @@ -7,7 +14,6 @@ - Fixed issue with Mystery for OW boolean options - ~~Merged DR v0.5.1.0 - Major Keylogic Update~~ - ### 0.1.8.1 - Fixed issue with activating flute in DW (OW Mixed) - Fixed issue with Parallel+Crossed not generating diff --git a/OverworldShuffle.py b/OverworldShuffle.py index d705f1a1..502dedb7 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.8.2-u' +__version__ = '0.1.9.0-u' def link_overworld(world, player): # setup mandatory connections diff --git a/README.md b/README.md index 7bf01573..d9e5e15f 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,19 @@ See https://alttpr.com/ for more details on the normal randomizer. This is a very new mode of LTTPR so the tools and info is very limited. - There is an [OW Rando Cheat Sheet](https://zelda.codemann8.com/images/shared/ow-rando-reference-sheet.png) that shows all the transitions that exist and are candidates for shuffle. - There is OW tracking capability within the following trackers: - - CodeTracker, an [EmoTracker](https://emotracker.net) package for LTTPR - [Community Tracker](https://alttptracker.dunka.net/) + - CodeTracker, an [EmoTracker](https://emotracker.net) package for LTTPR - There is an [OW OWG Reference Sheet](https://zelda.codemann8.com/images/shared/ow-owg-reference-sheet.png) that shows all the in-logic places where boots/mirror clips and fake flippers are expected from the player. # Known Issues -(Updated 2021-06-23) +(Updated 2021-08-26) ### If you want to playtest this, know these things: - Big Red Bomb may require bomb duping as ledge drops may be in the way of your path to the Pyramid Fairy crack - If you fake flipper, beware of transitioning south. You could end up at the top of the waterfall in the southeast of either world. If you mistakenly drop down, it is important to NOT make any other movements and S+Q immediately when the game allows you to (might take several seconds, the game has to scroll back to the original point of water entry) or there will be a hardlock. Falling from the waterfall is avoidable but it is super easy to do as it is super close to the transition. -- In Crossed OW Tile Swap, there are some interesting bunny water-walk situations that can occur, these are meant to be out-of-logic but beware of logic bugs around this area. +- In Crossed OW, there are some interesting bunny swimming situations that can occur, these are meant to be out-of-logic but beware of logic bugs around this area. But also, hardlocks can occur; if you take damage, be sure to S+Q immediately before moving in any direction, or you may get an infinite screen wrap glitch. ### Known bugs: -- ~~In Mixed OW Tile Swap, Smith and Stumpy have issues when their tiles are swapped. Progression cannot be found on them when these tiles are swapped~~ (Fixed in 0.1.6.4) - Screens that loop on itself and also have free-standing items, the sprites are duplicated and can cause item duplication - When OWG are performed to enter mega-tile screens (large OW screens), there is a small chance that an incorrect VRAM reference value causes the map graphics to offset in increments of 16 pixels @@ -66,26 +65,58 @@ OW Transitions are shuffled, but both worlds will have a matching layout. OW Transitions are shuffled within each world separately. -## Crossed (--ow_crossed) +## Crossed Options (--ow_crossed) This allows OW connections to be shuffled cross-world. -## Visual Representation of Main OW Shuffle Settings +Polar and Grouped both are guaranteed to result in two separated planes of tiles. To navigate to the other plane, you have the following methods: 1) Normal portals 2) Mirroring on DW tiles 3) Fluting to a LW tile that was previously unreachable -![OW Shuffle Settings Combination](https://zelda.codemann8.com/images/shared/ow-modes.gif) +Limited and Chaos are not bound to follow a two-plane framework. This means that it could be possible to travel on foot to every tile without entering a normal portal. + +See each option to get more details on the differences. + +### None + +Transitions will remain same-world. + +### Polar + +Only effective if Mixed/Tile Swap is enabled. Enabling Polar preserves the original/vanilla connections even when tiles are swapped/mixed. This results in a completely vanilla overworld, except that some tiles will transform Link to a Bunny (as per Mixed swapping some tiles to the other world). This offers an interesting twist on Mixed where you have a pre-conditioned knowledge of the terrain you will encounter, but not necessarily be able to do what you need to do there. (see Tile Swap/Mixed section for more details) + +### Grouped + +This option shuffles connections cross-world in the same manner as Tile Swap/Mixed, the connections leading in and coming out of a group of tiles are crossed. Unlike Polar, this uses a different set of tile groups as a basis of crossing connections, albeit the same rule govern which groups of tiles must cross together (see Tile Swap/Mixed for more details) + +### Limited + +Every transition independently is a candidate to be chosen as a cross-world connection, however only 9 transitions become crossed (in each world). This option abides by the Keep Similar Edges Together option and will guarantee same effect on all edges in a Similar Edge group if enabled. If a Similar Edge group is chosen from the pool of candidates, it only counts as one portal, not multiple. + +Note: Only parallel connections (a connection that also exists in the opposite world) are considered for cross-world connections, which means that the same connection in the opposite world will also connect cross-world. + +Motive: Why 9 connections? To imitate the effect of the 9 standard portals that exist. + +### Chaos + +Same as Limited, except that there is no limit to the number of cross-world connections that are made. Each transition has an equal 50/50 chance of being a crossed connection. ## Keep Similar Edges Together (--ow_keepsimilar) This keeps similar edge transitions together. ie. The 2 west edges of Potion Shop will be paired to another set of two similar edges -## Mixed Overworld (--ow_mixed) +Note: This affects OW Layout Shuffle mostly, but also affects Limited and Chaos modes in Crossed OW. -OW tiles are randomly chosen to become a part of the opposite world +## Tile Swap / Mixed Overworld (--ow_mixed) + +OW tiles are randomly chosen to become a part of the opposite world. When on the Overworld, there will be an L or D in the upper left corner, indicating which world you are currently in. Mirroring still works the same, you must be in the DW to mirror to the LW. + +Note: Tiles are put into groups that must be shuffled together when certain settings are enabled. For instance, if ER is disabled, then any tiles that have a connector cave that leads to another tile, those tiles must swap together; (an exception to this is the Old Man Rescue cave which has been modified similar to how Inverted modifies it, Old Man Rescue is ALWAYS accessible from the Light World) ## Flute Shuffle (--ow_fluteshuffle) When enabled, new flute spots are generated and gives the player the option to cancel out of the flute menu by pressing X. +Note: Desert Teleporter Ledge is always guaranteed to be chosen. One of the three Mountain tiles are guaranteed if OW Layout Shuffle is set to Vanilla. + ### Vanilla Flute spots remain unchanged. @@ -114,10 +145,10 @@ Show the help message and exit. For specifying the overworld layout shuffle you want as above. (default: vanilla) ``` ---ow_crossed +--ow_crossed ``` -This allows cross-world connections on the overworld +For specifying the type of cross-world connections you want on the overworld ``` --ow_keepsimilar From 5614dea2b51703f9bdd844930ed9638e6e494e25 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:21:10 -0600 Subject: [PATCH 091/104] Fix can_beat_game error Add start_region awareness to door finder combinations Added dungeon table --- BaseClasses.py | 2 -- DoorShuffle.py | 8 ++++++++ Dungeons.py | 44 ++++++++++++++++++++++++++++++++------------ KeyDoorShuffle.py | 13 +++++++------ 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b9b3dd1c..f92203d9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -438,8 +438,6 @@ class World(object): return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) if self.has_beaten_game(state): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ed21895..ca377c65 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1456,6 +1456,14 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True random.shuffle(sample_list) proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num) + # eliminate start region if portal marked as destination + excluded = {} + for region in start_regions: + portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None) + if portal and portal.destination: + excluded[region] = None + start_regions = [x for x in start_regions if x not in excluded.keys()] + key_layout = build_key_layout(builder, start_regions, proposal, world, player) while not validate_key_layout(key_layout, world, player): itr += 1 diff --git a/Dungeons.py b/Dungeons.py index 73f53794..6fe38cfb 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -375,6 +375,38 @@ flexible_starts = { 'Skull Woods': ['Skull Left Drop', 'Skull Pinball'] } + +class DungeonInfo: + + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None): + # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): + self.free_items = free + self.key_num = keys + self.bk_present = bk + self.map_present = map + self.compass_present = compass + self.bk_drops = bk_drop + self.key_drops = drops + self.prize = prize + + +dungeon_table = { + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None), +} + + dungeon_keys = { 'Hyrule Castle': 'Small Key (Escape)', 'Eastern Palace': 'Small Key (Eastern Palace)', @@ -407,18 +439,6 @@ dungeon_bigs = { 'Ganons Tower': 'Big Key (Ganons Tower)' } -dungeon_prize = { - 'Eastern Palace': 'Eastern Palace - Prize', - 'Desert Palace': 'Desert Palace - Prize', - 'Tower of Hera': 'Tower of Hera - Prize', - 'Palace of Darkness': 'Palace of Darkness - Prize', - 'Swamp Palace': 'Swamp Palace - Prize', - 'Skull Woods': 'Skull Woods - Prize', - 'Thieves Town': 'Thieves Town - Prize', - 'Ice Palace': 'Ice Palace - Prize', - 'Misery Mire': 'Misery Mire - Prize', - 'Turtle Rock': 'Turtle Rock - Prize', -} dungeon_hints = { 'Hyrule Castle': 'in Hyrule Castle', diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index cf18f4f4..c5aab0fc 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -4,8 +4,9 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events -from Dungeons import dungeon_keys, dungeon_bigs, dungeon_prize -from DungeonGenerator import ExplorationState, special_big_key_doors +from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table +from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import reserved_location, blind_boss_unavail class KeyLayout(object): @@ -1387,7 +1388,7 @@ def validate_key_layout(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1550,7 +1551,7 @@ def create_key_counters(key_layout, world, player): dungeon_entrance, portal_door = find_outside_connection(region) if (len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and key_layout.key_logic.dungeon in dungeon_prize): + and dungeon_table[key_layout.key_logic.dungeon].prize): state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance key_layout.prize_relevant = True @@ -1975,8 +1976,8 @@ def validate_key_placement(key_layout, world, player): len(counter.key_only_locations) + keys_outside if key_layout.prize_relevant: found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) - if not found_prize and key_layout.sector.name in dungeon_prize: - prize_loc = world.get_location(dungeon_prize[key_layout.sector.name], player) + if not found_prize and dungeon_table[key_layout.sector.name].prize: + prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) # todo: pyramid fairy only care about crystals 5 & 6 found_prize = 'Crystal' not in prize_loc.item.name else: From 1f884649d1dd9bcf1d735f74e639f91e202645f3 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:32:35 -0600 Subject: [PATCH 092/104] Remove unnecessary imports --- KeyDoorShuffle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index c5aab0fc..be4f03a2 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,8 +5,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table -from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event -from DungeonGenerator import reserved_location, blind_boss_unavail +from DungeonGenerator import ExplorationState, special_big_key_doors class KeyLayout(object): From 759e9979da18e5038a63ffe7d3f5e70013709aa2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 26 Aug 2021 15:51:12 -0600 Subject: [PATCH 093/104] Rule fix for bigkey shuffle, use the always_allow for the big key, but require it for every other item --- Rules.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Rules.py b/Rules.py index 28d627da..3114bde0 100644 --- a/Rules.py +++ b/Rules.py @@ -1950,9 +1950,11 @@ def add_key_logic_rules(world, player): forbid_item(location, d_logic.small_key_name, player) for door in d_logic.bk_doors: add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) - if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1: - for chest in d_logic.bk_chests: - add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + for chest in d_logic.bk_chests: + big_chest = world.get_location(chest.name, player) + add_rule(big_chest, create_rule(d_logic.bk_name, player)) + if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: + set_always_allow(big_chest, lambda state, item: item.name == d_logic.bk_name and item.player == player) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: From 9e7223795f7205edede0354e9dba754e3424aade Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 27 Aug 2021 15:03:27 -0600 Subject: [PATCH 094/104] Fix for path checking. should get pinball more often --- DoorShuffle.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index ca377c65..39d6283f 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1915,14 +1915,18 @@ def check_required_paths(paths, world, player): if dungeon_name in world.dungeon_layouts[player].keys(): builder = world.dungeon_layouts[player][dungeon_name] if len(paths[dungeon_name]) > 0: - states_to_explore = defaultdict(list) + states_to_explore = {} for path in paths[dungeon_name]: if type(path) is tuple: - states_to_explore[tuple([path[0]])] = path[1] + states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - states_to_explore[tuple(builder.path_entrances)].append(path) + common_starts = tuple(builder.path_entrances) + if common_starts not in states_to_explore: + states_to_explore[common_starts] = ([], 'all') + states_to_explore[common_starts][0].append(path) cached_initial_state = None - for start_regs, dest_regs in states_to_explore.items(): + for start_regs, info in states_to_explore.items(): + dest_regs, path_type = info if type(dest_regs) is not list: dest_regs = [dest_regs] check_paths = convert_regions(dest_regs, world, player) @@ -1939,11 +1943,17 @@ def check_required_paths(paths, world, player): cached_initial_state = state else: state = cached_initial_state - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: if check_for_pinball_fix(state, bad_region, world, player): explore_state(state, world, player) - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name)) @@ -1983,7 +1993,7 @@ def explore_state_not_inaccessible(state, world, player): state.add_all_doors_check_unattached(connect_region, world, player) -def check_if_regions_visited(state, check_paths): +def check_if_any_regions_visited(state, check_paths): valid = False breaking_region = None for region_target in check_paths: @@ -1995,6 +2005,13 @@ def check_if_regions_visited(state, check_paths): return valid, breaking_region +def check_if_all_regions_visited(state, check_paths): + for region_target in check_paths: + if not state.visited_at_all(region_target): + return False, region_target + return True, None + + def check_for_pinball_fix(state, bad_region, world, player): pinball_region = world.get_region('Skull Pinball', player) # todo: lobby shuffle From 91fbcd38dad5f700fc73740a03cfa49bd86f7e4e Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 27 Aug 2021 15:13:37 -0600 Subject: [PATCH 095/104] Fix shop hints in ER modes where shops are not located vanilla --- ItemList.py | 10 ++++++++++ Regions.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ItemList.py b/ItemList.py index 5f5b5e7e..eda7291f 100644 --- a/ItemList.py +++ b/ItemList.py @@ -604,6 +604,7 @@ def customize_shops(world, player): upgrade.location = loc change_shop_items_to_rupees(world, player, shops_to_customize) balance_prices(world, player) + check_hints(world, player) def randomize_price(price): @@ -707,6 +708,15 @@ def balance_prices(world, player): # loc.parent_region.shop.inventory[slot]['price'] = new_price +def check_hints(world, player): + if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']: + for shop, location_list in shop_to_location_table.items(): + if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: + continue # near the queen, near potions, and near 7 chests are fine + for loc_name in location_list: # other shops are indistinguishable in ER + world.get_location(loc_name, player).hint_text = f'for sale' + + repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart', 'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion'] diff --git a/Regions.py b/Regions.py index 35a7eda3..546cd49b 100644 --- a/Regions.py +++ b/Regions.py @@ -1374,9 +1374,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'), - 'Potion Shop - Left': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Middle': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Right': (None, None, False, 'for sale near the witch'), + 'Potion Shop - Left': (None, None, False, 'for sale near potions'), + 'Potion Shop - Middle': (None, None, False, 'for sale near potions'), + 'Potion Shop - Right': (None, None, False, 'for sale near potions'), } lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} From 30aeeedb6e4ef6dd324d78417c937369af9b482e Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 27 Aug 2021 16:43:16 -0500 Subject: [PATCH 096/104] Fixed missing flipper rule for IP Leave --- Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Rules.py b/Rules.py index abc447ba..cb943648 100644 --- a/Rules.py +++ b/Rules.py @@ -828,6 +828,7 @@ def default_rules(world, player): set_rule(world.get_entrance('Ice Lake Northeast Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Ice Lake Southwest Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Ice Lake Southeast Water Drop', player), lambda state: state.has('Flippers', player)) + set_rule(world.get_entrance('Ice Lake Moat Water Entry', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Shopping Mall SW', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Bomber Corner Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Bomber Corner Waterfall Water Drop', player), lambda state: state.has('Flippers', player)) From e03fa2f542b48cd94140ec50f371c0cd2197b54f Mon Sep 17 00:00:00 2001 From: codemann8 Date: Fri, 27 Aug 2021 18:30:36 -0500 Subject: [PATCH 097/104] Fixed missing flipper rule for IP Leave --- Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Rules.py b/Rules.py index cb943648..5aa9d9a6 100644 --- a/Rules.py +++ b/Rules.py @@ -1325,6 +1325,7 @@ def ow_bunny_rules(world, player): add_bunny_rule(world.get_entrance('Ice Lake Northeast Water Drop', player), player) add_bunny_rule(world.get_entrance('Ice Lake Southwest Water Drop', player), player) add_bunny_rule(world.get_entrance('Ice Lake Southeast Water Drop', player), player) + add_bunny_rule(world.get_entrance('Ice Lake Moat Water Entry', player), player) add_bunny_rule(world.get_entrance('Shopping Mall SW', player), player) add_bunny_rule(world.get_entrance('Bomber Corner Water Drop', player), player) add_bunny_rule(world.get_entrance('Bomber Corner Waterfall Water Drop', player), player) From 67c4fee636b4fafb661aa612e5d8454e2b70db60 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 30 Aug 2021 15:20:27 -0600 Subject: [PATCH 098/104] Boss shuffle fix - fixes some bias discovered by krebel --- Bosses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Bosses.py b/Bosses.py index 2718431e..90acafcf 100644 --- a/Bosses.py +++ b/Bosses.py @@ -181,11 +181,11 @@ def place_bosses(world, player): logging.getLogger('').debug('Bosses chosen %s', bosses) - random.shuffle(bosses) for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') - boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None) - if not boss: + try: + boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) + except IndexError: raise FillError('Could not place boss for location %s' % loc_text) bosses.remove(boss) From 07287d85a765440e539f0e741ba937de6f4b8fe4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 30 Aug 2021 15:21:25 -0600 Subject: [PATCH 099/104] Improve exclusion calculation --- BaseClasses.py | 1 + DoorShuffle.py | 2 +- DungeonGenerator.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index f92203d9..820b2fed 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -77,6 +77,7 @@ class World(object): self._room_cache = {} self.dungeon_layouts = {} self.inaccessible_regions = {} + self.enabled_entrances = {} self.key_logic = {} self.pool_adjustment = {} self.key_layout = defaultdict(dict) diff --git a/DoorShuffle.py b/DoorShuffle.py index 39d6283f..e89a7bb4 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -753,7 +753,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player): entrances_map, potentials, connections = connections_tuple - enabled_entrances = {} + enabled_entrances = world.enabled_entrances[player] = {} sector_queue = deque(dungeon_builders.values()) last_key, loops = None, 0 logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon")) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 948115f3..f579f285 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -109,7 +109,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon p_region = portal.door.entrance.connected_region access_region = next(x.parent_region for x in p_region.entrances if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) - if access_region.name in world.inaccessible_regions[player]: + if (access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]): excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} From dad6016498ea22ae9f5591c24498a8caa8e5e39d Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 30 Aug 2021 15:57:45 -0600 Subject: [PATCH 100/104] Rom features: update cutofftable Add a multiworld ram slot for item that just got grabbed --- Rom.py | 2 +- asm/doortables.asm | 2 ++ data/base2current.bps | Bin 136271 -> 136284 bytes 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index d39675db..e2eeb5f1 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da' +RANDOMIZERBASEHASH = 'f0947723c296ce13c7f4c909d8318042' class JsonRom(object): diff --git a/asm/doortables.asm b/asm/doortables.asm index 8ee0dd60..eae16430 100644 --- a/asm/doortables.asm +++ b/asm/doortables.asm @@ -582,6 +582,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052 ; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e +; Sewer Drop, Mire Cross, GT Crystal Circles +dw $0011, $00b2, $003d dw $ffff ; dungeon tables diff --git a/data/base2current.bps b/data/base2current.bps index 5f41171067dccc266f98a3c0b2daee5a07a03f06..ddbb10748c14ecc4470df7ce1e0920cd009f2c01 100644 GIT binary patch delta 668 zcmV;N0%QHps0iGs2(Ur{1PKWC_p?R;Dn0>ClW{*T1dp4*ev_X+H8!z_$xhG?uY2=; z@CB()n*<>CiC)lFlOuq!6t7wHe$SAAFpvtbk)H;DlS^&yk^j&LsY#nOlL2mjwY)lPpeIHaPx?{m>kfAAo@qqgnZZ z36Fx4Ncowm1Z&Cu&^((OfS)J+fenv>lP3O~CH|EIsY7eY0GDEuyG|Smb^EKiffbK} zmtK?MP8b|B`+*OSf|oG*pD+5Y1F0!%sVZx021G?h6M-g^6;CfA6_bG#qd5JWjFGgb zplfKCu7$yBbx;cfQ=bAfK|W3 zlR^xDkw1Vu$zZ=@3xJabvwKhh0}T%ufSCc00Z@N{2AdkQiBkp@2TK)!fq;_}vzBNL z1OYdbq--w%7PHiB5(*aySAo6o1jz!h323PdpaEPN;st0ofDE(!cGEWnaU+kHHj~w^ z@BvAa#;-2{9JBVX6etlXF@dQVZzc48k2g?%uO^>Xvm4k8B>_>BC;cuG0A2tQ0I~o* zFNP`;m3#I9Pl1zN{UQQiKC^-SgMtl#_nRJDmWB8Kn=g2lw-bN?G?WP6-~iF^d{Tlm Cr4)St delta 632 zcmWO3Jxmi}90qXT_IegdOQ00WmnD2D2v3 z4Fi_)*Hm&b#W2WO@i1pX90;J(#eqv)l<4AM;$#zu;lrMG{XNh0Jl~eITT9v%H=@>b z?p@J~>K*6|j5`@vbhflCKazT7gK&2zoYL?a5r4HkwJC z>#PogLBxKf))y7yc@#zdDNJ*f;^RKrbhOVz2nL Date: Tue, 31 Aug 2021 14:10:47 -0600 Subject: [PATCH 101/104] Minor rom fix --- Rom.py | 2 +- data/base2current.bps | Bin 136284 -> 136284 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index e2eeb5f1..53d6666e 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'f0947723c296ce13c7f4c909d8318042' +RANDOMIZERBASEHASH = '091671c7f555331ff2f411b304141c29' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index ddbb10748c14ecc4470df7ce1e0920cd009f2c01..4571d623111492b36542f9572a826fd4ecbbef1d 100644 GIT binary patch delta 57 zcmV-90LK5^s0iGs2(U5D0acSS%_JAEJAg34An*#WX!CwQslBA|7K)b}I{Sld&9`mM P0p~^tEZfI>QeH Date: Tue, 31 Aug 2021 15:36:38 -0600 Subject: [PATCH 102/104] Couple of big key logic fixes - one with bk forced Another with bk assumed to be found early in dungeon Catobat map check fix from rom --- BaseClasses.py | 1 + KeyDoorShuffle.py | 15 +++++++++++++-- Main.py | 2 +- RELEASENOTES.md | 11 +++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 820b2fed..b5462eb4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -142,6 +142,7 @@ class World(object): set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) set_player_attr('exp_cache', defaultdict(dict)) + set_player_attr('enabled_entrances', {}) 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/KeyDoorShuffle.py b/KeyDoorShuffle.py index be4f03a2..23dd24be 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -282,6 +282,12 @@ def analyze_dungeon(key_layout, world, player): if not big_avail: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) + # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop + # I don't like this exception here or there + elif available <= possible_smalls and avail_bigs and non_big_locs > 0: + max_ctr = find_max_counter(key_layout) + bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] + key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): @@ -1619,8 +1625,13 @@ def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player): # ttl_small_key_only = len(counter.key_only_locations) return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0 elif door.bigKey: - available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) - return not counter.big_key_opened and available_big_locations > 0 and not layout.big_key_special + if counter.big_key_opened: + return False + if layout.big_key_special: + return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey) + else: + available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) + return available_big_locations > 0 else: return True diff --git a/Main.py b/Main.py index ee9e83fb..be2cb2e6 100644 --- a/Main.py +++ b/Main.py @@ -28,7 +28,7 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from Utils import output_path, parse_player_names -__version__ = '0.5.1.0-u' +__version__ = '0.5.1.1-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 37877c89..4d212275 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,17 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.1 + * Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore + * Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame) + * Fix for bias in boss shuffle "full" + * Fix for certain lone big chests in keysanity (allowed you to get contents without big key) + * Fix for pinball checking + * Fix for multi-entrance dungeons + * 2 fixes for big key placement logic + * ensure big key is placed early if the validator assumes it) + * Open big key doors appropriately when generating rules and big key is forced somewhere + * Updated cutoff entrances for intensity 3 * 0.5.1.0 * Large logic refactor introducing a new method of key logic * Some performance optimization From 4567901cd904bf43e124ee850774924e59960ac1 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Tue, 31 Aug 2021 19:29:26 -0500 Subject: [PATCH 103/104] Base ROM changes from upstream DR --- Rom.py | 2 +- data/base2current.bps | Bin 141185 -> 141208 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index c9ebb8ad..d0d2d65d 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '712ebda1ef6818c59cde0371b5a4e4a9' +RANDOMIZERBASEHASH = '9e8b765fca0f00b54e5be78bb6eb62a3' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 997df3d6bc406f60caf48ec8383651712d25f0be..47c59ecdeaa2513f5c3613d063a848503ecb4f56 100644 GIT binary patch delta 793 zcmWNNTS!xJ9LM)}XKT*8>&y$KCvT*gn$@(MZY*u9)@EsIUA(-AaFG83h>{*2TJpv;qSMcCe@5Gt<4}yBt`B>bypfx@=B7S8 z9?)Vq)VDZJ6`g4J?|^sa!YnHhOTxxq-txJGWc*qJzs(Pk8}3-PArnL_1t|xy<3<(9 zZkh;rq-9G6%I7R!KWHboyMWQeRrOo7!ie;$I>dt;Hd)J%9cmC{MOpg~>m^N7b}i;wgaIJV4Go&L&`ZMlhJ~|hp?O#!TZQ4f94}V! z^0Kl8f`koOS4l>qn^yvjE05J;IWJc5ymo(6vw@VsXJ@sR1;XC9L8gmc=lMsH;bI&r zT_%Hp!2wQX>RaSou;-R zA~eHfIEI>FY`Qw}@2lzV0@MbZA|*NDJ-Fr#7t@D(mb{HCGY?|MF=5N@Rq1r(p+0-p zHX`PpEB)p5)wwQ*%K{g(BzC^G6t#ejR%NT&RavTMs(sZX6^2_we~^=amlh>;G)VX8 z^O^dgP7f}}MT2cL5OvdNG_pt&D_Z!i4|Gyhlv~WCDymwbQ?!#lq8zP}+Vf{GT%p4C z8MN6*gNd3+brjK&1kpD7oz77~o&K9H6!Ta1hGSOQ5IO#O;-s@)@tW;t{$G>pZ~+dZ#1YTLG@xCQcmTZ z`o!7LS~)6%J(XtYsNBDQ{ubXL#IaX5oa<3cqi-<#m%Cj!0U3BHg>Bt%2Jhj$_cZLV zUj$3v&tXYWGj+Z~cvG33c$hFbtbWtE`=tz(J|~7ydfG0!!$KxxW0E|1@ehgd zx}fvV7^9?kQ8?mD$NmbDXk}O~`pU!y3dTb55%hrg76}Tbn&KnnQA06SikF0Ph{Ys> z*8ZD&Ms!{meHorKre@avCGql78}c6w%`2GQ6l0<&RdtOunV8{AW0BDSL^o*<`|b`u zpPrx`^uiE7GkHV)s$jA!OkyBHf@1rDa^R&6{cdcTXr z!hj0T^UTCCJc}v5UC=KVsIa#%4nM-ZxQXu&cKQrj>`6O_TUK5{+crF0fsgQ=D_3JN zWO3Feq|ok4!(+&c?ANi%SHd+niz(4(b%bDu3CklIyRgYp}^F-{x*FrE5vmL zr!`4c#e_+{kh|&hmfEkgdH#qbE`7BmS3Yl(%<2)#{(!|2c`r(mLRDmB;ADOQK&QG9vA&N ffCe;h$xWvvkcEoX Date: Tue, 31 Aug 2021 19:44:58 -0500 Subject: [PATCH 104/104] Version bump 0.1.9.1 --- CHANGELOG.md | 4 ++++ OverworldShuffle.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f42918..1d6898aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 0.1.9.1 +- Fixed logic issue with leaving IP entrance not requiring flippers +- ~~Merged DR v0.5.1.1 - Map Indicator Fix/Boss Shuffle Bias/Shop Hints~~ + ### 0.1.9.0 - Expanded Crossed OW to four separate options, see Readme for details - Crossed OW will now play a short SFX when changing worlds diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 502dedb7..558f089f 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -2,7 +2,7 @@ import RaceRandom as random, logging, copy from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OpenStd, parallel_links, IsParallel -__version__ = '0.1.9.0-u' +__version__ = '0.1.9.1-u' def link_overworld(world, player): # setup mandatory connections