Merge commit 'c4ae75bc2b866e4a304614200eac678fd4da4aee' into multidoors

This commit is contained in:
compiling
2020-01-16 20:23:43 +11:00
13 changed files with 304 additions and 509 deletions

View File

@@ -7,7 +7,7 @@ from Rom import LocalRom, apply_rom_settings
def adjust(args):
start = time.perf_counter()
start = time.process_time()
logger = logging.getLogger('')
logger.info('Patching ROM.')
@@ -26,6 +26,6 @@ def adjust(args):
rom.write_to_file(output_path('%s.sfc' % outfilebase))
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.perf_counter() - start)
logger.debug('Total Time: %s', time.process_time() - start)
return args

View File

@@ -6,7 +6,6 @@ from collections import OrderedDict, deque
from EntranceShuffle import door_addresses
from _vendor.collections_extended import bag
from EntranceShuffle import door_addresses
from Utils import int16_as_bytes
from Tables import normal_offset_table, spiral_offset_table
from RoomData import Room
@@ -122,7 +121,7 @@ class World(object):
except KeyError:
for region in self.regions:
if region.name == regionname and region.player == player:
assert not region.world # this should only happen before initialization
assert not region.world # this should only happen before initialization
return region
raise RuntimeError('No such region %s for player %d' % (regionname, player))

View File

@@ -640,6 +640,14 @@ def cross_dungeon(world, player):
paths = determine_required_paths(world, player)
check_required_paths(paths, world, player)
hc = world.get_dungeon('Hyrule Castle', player)
del hc.dungeon_items[0] # removes map
hc.dungeon_items.append(ItemFactory('Compass (Escape)', player))
at = world.get_dungeon('Agahnims Tower', player)
at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player))
gt = world.get_dungeon('Ganons Tower', player)
del gt.dungeon_items[0] # removes map
start = time.process_time()
total_keys = remaining = 29
total_candidates = 0
@@ -703,6 +711,7 @@ def cross_dungeon(world, player):
queue = collections.deque(sorted(queue, key=lambda b: b.combo_size))
else:
logger.info('Cross Dungeon: Increase failed for %s', name)
builder.key_doors_num -= 1
builder.flex = 0
logger.info('Cross Dungeon: Keys unable to assign in pool %s', remaining)
@@ -1185,7 +1194,7 @@ def add_inaccessible_doors(world, player):
create_door(world, player, 'Death Mountain Return Cave (West)', 'Death Mountain Return Ledge')
if 'Desert Palace Lone Stairs' in world.inaccessible_regions[player]:
create_door(world, player, 'Desert Palace Entrance (East)', 'Desert Palace Lone Stairs')
if world.mode == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
if world.mode[player] == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
create_door(world, player, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Ledge')
create_door(world, player, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Ledge')
@@ -2020,36 +2029,18 @@ default_one_way_connections = [
]
# For crossed
default_dungeon_sets = [
['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Sewers Rat Path', 'Sanctuary',
'Hyrule Dungeon Cellblock'],
['Eastern Lobby', 'Eastern Boss'],
['Desert Back Lobby', 'Desert Boss', 'Desert Main Lobby', 'Desert West Lobby', 'Desert East Lobby'],
['Hera Lobby', 'Hera Boss'],
['Tower Lobby', 'Tower Agahnim 1'],
['PoD Lobby', 'PoD Boss'],
['Swamp Lobby', 'Swamp Boss'],
['Skull 1 Lobby', 'Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull 2 East Lobby',
'Skull 2 West Lobby', 'Skull Back Drop', 'Skull 3 Lobby', 'Skull Boss'],
['Thieves Lobby', 'Thieves Attic Window', 'Thieves Blind\'s Cell', 'Thieves Boss'],
['Ice Lobby', 'Ice Boss'],
['Mire Lobby', 'Mire Boss'],
['TR Main Lobby', 'TR Boss', 'TR Eye Bridge', 'TR Big Chest Entrance', 'TR Lazy Eyes'],
['GT Lobby', 'GT Agahnim 2']
]
dungeon_x_idx_to_name = {
0: 'Hyrule Castle',
1: 'Eastern Palace',
2: 'Desert Palace',
3: 'Tower of Hera',
4: 'Agahnims Tower',
5: 'Palace of Darkness',
6: 'Swamp Palace',
7: 'Skull Woods',
8: 'Thieves Town',
9: 'Ice Palace',
10: 'Misery Mire',
11: 'Turtle Rock',
12: 'Ganons Tower'
compass_data = { # offset from 0x122e17, sram storage, write offset from 1037b9, 0 = jmp or # of nops
'Hyrule Castle': (0x1, 0xc0, 0x16, 0),
'Eastern Palace': (0x1C, 0xc1, 0x28, 0),
'Desert Palace': (0x35, 0xc2, 0x4a, 0),
'Agahnims Tower': (0x51, 0xc3, 0x5c, 0),
'Swamp Palace': (0x6A, 0xc4, 0x7e, 0),
'Palace of Darkness': (0x83, 0xc5, 0xa4, 0),
'Misery Mire': (0x9C, 0xc6, 0xca, 0),
'Skull Woods': (0xB5, 0xc7, 0xf0, 0),
'Ice Palace': (0xD0, 0xc8, 0x102, 0),
'Tower of Hera': (0xEB, 0xc9, 0x114, 0),
'Thieves Town': (0x106, 0xca, 0x138, 0),
'Turtle Rock': (0x11F, 0xcb, 0x15e, 0),
'Ganons Tower': (0x13A, 0xcc, 0x170, 2)
}

View File

@@ -1105,10 +1105,13 @@ def weighted_random_locations(dungeon_map, free_location_sectors):
def minimal_locations(dungeon_name):
# bump to 5 if maps do something useful for all these dungeons
if dungeon_name == 'Hyrule Castle':
return 3
return 4 # bk + compass + 2 others
if dungeon_name == 'Agahnims Tower':
return 2 # bump to 3 once compasses work
return 4
if dungeon_name == 'Ganons Tower':
return 4
# reduce gt to 4 once compasses work
return 5

View File

@@ -149,7 +149,7 @@ def parse_arguments(argv, no_defaults=False):
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
parser.add_argument('--shuffle', default=defval('vanilla'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
@@ -173,7 +173,7 @@ def parse_arguments(argv, no_defaults=False):
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--door_shuffle', default=defval('vanilla'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'],
parser.add_argument('--door_shuffle', default=defval('basic'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'],
help='''\
Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon.

View File

@@ -125,10 +125,11 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Map (Tower of Hera)': (False, True, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'),
'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Hyrule Castle'),
'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),

View File

@@ -1,5 +1,4 @@
import collections
from collections import defaultdict
from collections import defaultdict, deque
from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs
@@ -108,42 +107,61 @@ def analyze_dungeon(key_layout, world, player):
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
original_key_counter = find_counter({}, False, key_layout)
queue = collections.deque([(None, original_key_counter)])
queue = deque([(None, original_key_counter)])
doors_completed = set()
visited_cid = set()
visited_cid.add(cid(original_key_counter, key_layout))
while len(queue) > 0:
queue = collections.deque(sorted(queue, key=queue_sorter))
queue = deque(sorted(queue, key=queue_sorter))
parent_door, key_counter = queue.popleft()
chest_keys = available_chest_small_keys(key_counter, world, player)
raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
avail_bigs = exist_relevant_big_doors(key_counter, key_layout)
non_big_locs = count_locations_big_optional(key_counter.free_locations)
if not key_counter.big_key_opened:
if chest_keys == count_locations_big_optional(key_counter.free_locations) and available <= possible_smalls and not avail_bigs:
if chest_keys == non_big_locs and available <= possible_smalls and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
# todo: detect forced subsequent keys - see keypuzzles
# try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = collections.deque()
child_queue = deque()
smallest_rule = None
for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
if not empty_counter(odd_counter) and child not in doors_completed:
child_queue.append((child, odd_counter))
empty_flag = empty_counter(odd_counter)
child_queue.append((child, odd_counter, empty_flag))
if child in doors_completed and child in key_logic.door_rules.keys():
rule = key_logic.door_rules[child]
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
while len(child_queue) > 0:
child, odd_counter = child_queue.popleft()
if not child.bigKey:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False)
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)
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, key_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
doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout)
queue.append((child, next_counter))
check_rules(original_key_counter, key_layout)
ctr_id = cid(next_counter, key_layout)
if ctr_id not in visited_cid:
queue.append((child, next_counter))
visited_cid.add(ctr_id)
possible_smalls_collected = len(key_counter.key_only_locations) + non_big_locs
if not key_counter.big_key_opened:
if smallest_rule is not None and smallest_rule >= possible_smalls_collected and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
check_rules(original_key_counter, key_layout, world, player)
def count_key_drops(sector):
@@ -170,6 +188,8 @@ def queue_sorter_2(queue_item):
def find_bk_locked_sections(key_layout, world):
if key_layout.big_key_special:
return
key_counters = key_layout.key_counters
key_logic = key_layout.key_logic
@@ -217,7 +237,7 @@ def unique_child_door(child, key_counter):
return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk): # try to waste as many keys as possible?
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)
@@ -235,15 +255,14 @@ def find_best_counter(door, odd_counter, key_counter, key_layout, world, player,
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 relative_empty_counter(odd_counter, new_counter):
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:
if not key_wasted(new_door, last_counter, new_counter, key_layout, world, player):
ignored_doors.add(new_door)
else:
last_counter = new_counter
opened_doors = proposed_doors
bk_opened = bk_open
ignored_doors.add(new_door)
return last_counter
@@ -266,18 +285,20 @@ def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
return small_doors + big_doors
def key_wasted(new_door, old_counter, new_counter, key_layout, world, player):
def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, player):
if new_door.bigKey: # big keys are not wastes - it uses up a location
return True
chest_keys = available_chest_small_keys(old_counter, world, player)
old_avail = chest_keys + len(old_counter.key_only_locations) - old_counter.used_keys
old_key_diff = len(old_counter.key_only_locations) - old_counter.used_keys
old_avail = chest_keys + old_key_diff
new_chest_keys = available_chest_small_keys(new_counter, world, player)
new_avail = new_chest_keys + len(new_counter.key_only_locations) - new_counter.used_keys
if new_avail < old_avail:
new_key_diff = len(new_counter.key_only_locations) - new_counter.used_keys
new_avail = new_chest_keys + new_key_diff
if new_key_diff < old_key_diff or new_avail < old_avail:
return True
if new_avail == old_avail:
old_children = old_counter.child_doors.keys()
new_children = [x for x in new_counter.child_doors.keys() if x not in old_children and x.dest not in old_children]
new_children = [x for x in new_counter.child_doors.keys() if x != old_door and x.dest != old_door and (not x.bigKey or x not in old_children)]
current_counter = new_counter
opened_doors = dict(current_counter.open_doors)
bk_opened = current_counter.big_key_opened
@@ -285,7 +306,7 @@ def key_wasted(new_door, old_counter, new_counter, key_layout, world, player):
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)
if key_wasted(new_child, current_counter, new_counter, key_layout, world, player):
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
return True # waste is possible
return False
@@ -314,15 +335,12 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
chest_keys = available_chest_small_keys(key_counter, world, player)
key_gain = len(key_counter.key_only_locations) - len(prev_counter.key_only_locations)
raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
required_keys = min(available, possible_smalls) + key_counter.used_keys
# required_keys = key_counter.used_keys + 1 # this sometimes makes more sense
# if prev_avail < required_keys:
# required_keys = prev_avail + prev_counter.used_keys
# return DoorRules(required_keys)
# else:
# previous method
# raw_avail = chest_keys + len(key_counter.key_only_locations)
# available = raw_avail - key_counter.used_keys
# possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
# required_keys = min(available, possible_smalls) + key_counter.used_keys
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
adj_chest_keys = min(chest_keys, required_keys)
needed_chests = required_keys - len(key_counter.key_only_locations)
unneeded_chests = min(key_gain, adj_chest_keys - needed_chests)
@@ -349,6 +367,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player):
inverted_counter.free_locations = dict_difference(max_counter.free_locations, counter.free_locations)
inverted_counter.key_only_locations = dict_difference(max_counter.key_only_locations, counter.key_only_locations)
# child doors? used_keys?
# inverted_counter.child_doors = dict_difference(max_counter.child_doors, counter.child_doors)
inverted_counter.open_doors = dict_difference(max_counter.open_doors, counter.open_doors)
inverted_counter.other_locations = dict_difference(max_counter.other_locations, counter.other_locations)
for loc in inverted_counter.other_locations:
@@ -417,10 +436,21 @@ def available_chest_small_keys(key_counter, world, player):
return key_counter.max_chests
def bk_restricted_rules(rule, door, odd_counter, key_counter, key_layout, world, player):
def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
if not world.keyshuffle[player] and not world.retro[player]:
cnt = 0
for loc in key_counter.free_locations:
if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
cnt += 1
return min(cnt, key_counter.max_chests)
else:
return key_counter.max_chests
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)
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
bk_number = create_rule(best_counter, key_counter, key_layout, world, player).small_key_num
if bk_number == rule.small_key_num:
return
@@ -497,6 +527,9 @@ def exist_relevant_big_doors(key_counter, key_layout):
diff = dict_difference(bk_counter.key_only_locations, key_counter.key_only_locations)
if len(diff) > 0:
return True
diff = dict_difference(bk_counter.child_doors, key_counter.child_doors)
if len(diff) > 0:
return True
return False
@@ -553,14 +586,14 @@ def flatten_pair_list(paired_list):
return flat_list
def check_rules(original_counter, key_layout):
def check_rules(original_counter, key_layout, world, player):
all_key_only = set()
key_only_map = {}
queue = collections.deque([(None, original_counter, original_counter.key_only_locations)])
queue = deque([(None, original_counter, original_counter.key_only_locations)])
completed = set()
completed.add(cid(original_counter, key_layout))
while len(queue) > 0:
queue = collections.deque(sorted(queue, key=queue_sorter_2))
queue = deque(sorted(queue, key=queue_sorter_2))
access_door, counter, key_only_loc = queue.popleft()
for loc in key_only_loc:
if loc not in all_key_only:
@@ -606,6 +639,7 @@ def check_rules(original_counter, key_layout):
if check_non_bk:
adjust_key_location_mins(key_layout, min_rule_non_bk, lambda r: r.small_key_num if r.alternate_small_key is None else r.alternate_small_key,
lambda r, v: r if r.alternate_small_key is None else setattr(r, 'alternate_small_key', v))
check_rules_deep(original_counter, key_layout, world, player)
def adjust_key_location_mins(key_layout, min_rules, getter, setter):
@@ -632,6 +666,108 @@ def adjust_key_location_mins(key_layout, min_rules, getter, setter):
setter(rule, collected_keys)
def check_rules_deep(original_counter, key_layout, world, player):
key_logic = key_layout.key_logic
big_locations = {x for x in key_layout.all_chest_locations if x not in key_logic.bk_restricted}
queue = deque([original_counter])
completed = set()
completed.add(cid(original_counter, key_layout))
last_counter = None
bail = 0
while len(queue) > 0:
counter = queue.popleft()
if counter == last_counter:
bail += 1
if bail > 10:
raise Exception('Key logic issue, during deep rule check: %s' % key_layout.sector.name)
else:
bail = 0
last_counter = counter
chest_keys = available_chest_small_keys_logic(counter, world, player, key_logic.sm_restricted)
big_avail = counter.big_key_opened
big_maybe_not_found = not counter.big_key_opened
if not key_layout.big_key_special and not big_avail:
for location in counter.free_locations:
if location not in key_logic.bk_restricted:
big_avail = True
break
outstanding_big_locs = {x for x in big_locations if x not in counter.free_locations}
if big_maybe_not_found:
if len(outstanding_big_locs) == 0:
big_maybe_not_found = False
big_uses_chest = big_avail and not key_layout.big_key_special
collected_alt = len(counter.key_only_locations) + chest_keys
if big_uses_chest and chest_keys == count_locations_big_optional(counter.free_locations, counter.big_key_opened):
chest_keys -= 1
collected = len(counter.key_only_locations) + chest_keys
can_progress = len(counter.child_doors) == 0
smalls_opened = False
small_rules = []
for door in counter.child_doors.keys():
can_open = False
if door.bigKey and big_avail:
can_open = True
elif door.name in key_logic.door_rules.keys():
rule = key_logic.door_rules[door.name]
small_rules.append(rule)
if rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
can_open = True
smalls_opened = True
elif not door.bigKey:
can_open = True
if can_open:
can_progress = smalls_opened or not big_maybe_not_found
next_counter = find_next_counter(door, counter, key_layout)
c_id = cid(next_counter, key_layout)
if c_id not in completed:
completed.add(c_id)
queue.append(next_counter)
if not can_progress:
if len(small_rules) > 0: # zero could be indicative of a problem, but also, the big key is now required
reduce_rules(small_rules, collected, collected_alt)
queue.append(counter) # run it through again
else:
raise Exception('Possible problem with generation or bk rules')
def rule_satisfied(rule, collected, collected_alt, outstanding_big_locs, chest_keys, key_layout):
if collected >= rule.small_key_num:
return True
if rule.allow_small and collected >= rule.small_key_num-1 and chest_keys < key_layout.max_chests:
return True
rule_diff = outstanding_big_locs.difference(rule.alternate_big_key_loc)
if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected >= rule.alternate_small_key:
return True
if collected_alt > collected:
if collected_alt >= rule.small_key_num:
return True
if rule.allow_small and collected_alt >= rule.small_key_num-1 and chest_keys+1 < key_layout.max_chests:
return True
if rule.alternate_small_key is not None and len(rule_diff) == 0 and collected_alt >= rule.alternate_small_key:
return True
return False
def reduce_rules(small_rules, collected, collected_alt):
smallest_rules = []
min_num = None
for rule in small_rules:
if min_num is None or rule.small_key_num <= min_num:
if min_num is not None and rule.small_key_num < min_num:
min_num = rule.small_key_num
smallest_rules.clear()
elif min_num is None:
min_num = rule.small_key_num
smallest_rules.append(rule)
for rule in smallest_rules:
if rule.allow_small: # we are already reducing it
rule.allow_small = False
if min_num > collected_alt > collected:
rule.small_key_num = collected_alt
else:
rule.small_key_num = collected
# Soft lock stuff
def validate_key_layout(key_layout, world, player):
flat_proposal = key_layout.flat_prop
@@ -688,7 +824,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
def cnt_avail_small_locations(key_layout, ttl_locations, state, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
return min(ttl_locations - state.used_locations, state.key_locations - state.used_smalls)
return key_layout.max_chests + state.key_locations - state.used_smalls
return state.key_locations - state.used_smalls
def cnt_avail_big_locations(ttl_locations, state, world, player):
@@ -709,7 +845,7 @@ def create_key_counters(key_layout, 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)
queue = collections.deque([(key_counters[code], state)])
queue = deque([(key_counters[code], state)])
while len(queue) > 0:
next_key_counter, parent_state = queue.popleft()
for door in next_key_counter.child_doors:
@@ -820,7 +956,10 @@ def find_counter_hint(opened_doors, bk_hint, key_layout):
def find_max_counter(key_layout):
return find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
if len(max_counter.child_doors) > 0:
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout)
return max_counter
def counter_id(opened_doors, bk_unlocked, flat_proposal):
@@ -861,12 +1000,12 @@ def validate_vanilla_key_logic(world, player):
def val_hyrule(key_logic, world, player):
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 3)
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2)
# why is allow_small actually false? - because chest key is forced elsewhere?
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest')
# val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
# val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest')
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
def val_eastern(key_logic, world, player):
@@ -951,7 +1090,8 @@ def val_ice(key_logic, world, player):
def val_mire(key_logic, world, player):
mire_west_wing = {'Misery Mire - Big Key Chest', 'Misery Mire - Compass Chest'}
val_rule(key_logic.door_rules['Mire Spikes NW'], 5) # todo: is sometimes 3 or 5? best_counter order matters
val_rule(key_logic.door_rules['Mire Spikes NW'], 3) # todo: is sometimes 3 or 5? best_counter order matters
# val_rule(key_logic.door_rules['Mire Spike Barrier NE'], 4) # kind of a waste mostly
val_rule(key_logic.door_rules['Mire Hub WS'], 5, False, None, 3, mire_west_wing)
val_rule(key_logic.door_rules['Mire Conveyor Crystal WS'], 6, False, None, 4, mire_west_wing)
assert world.get_location('Misery Mire - Boss', player) in key_logic.bk_restricted
@@ -981,11 +1121,11 @@ def val_ganons(key_logic, world, player):
compass_room = {'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'}
gt_middle = {'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Chest', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Bob\'s Chest', 'Ganons Tower - Big Chest'}
val_rule(key_logic.door_rules['GT Double Switch EN'], 6, False, None, 4, rando_room.union({'Ganons Tower - Firesnake Room'}))
val_rule(key_logic.door_rules['GT Hookshot ES'], 8, True, 'Ganons Tower - Map Chest', 5, {'Ganons Tower - Map Chest'})
val_rule(key_logic.door_rules['GT Tile Room EN'], 7, False, None, 5, compass_room)
val_rule(key_logic.door_rules['GT Firesnake Room SW'], 8, False, None, 5, rando_room)
val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 8, False, None, 6, gt_middle) # should be 7?
val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 6) # not sure about 6 this...
val_rule(key_logic.door_rules['GT Hookshot ES'], 7, False, 'Ganons Tower - Map Chest', 5, {'Ganons Tower - Map Chest'})
val_rule(key_logic.door_rules['GT Tile Room EN'], 6, False, None, 5, compass_room)
val_rule(key_logic.door_rules['GT Firesnake Room SW'], 7, False, None, 5, rando_room)
val_rule(key_logic.door_rules['GT Conveyor Star Pits EN'], 6, False, None, 5, gt_middle) # should be 7?
val_rule(key_logic.door_rules['GT Mini Helmasaur Room WN'], 6) # not sure about this 6...
val_rule(key_logic.door_rules['GT Crystal Circles SW'], 8)
assert world.get_location('Ganons Tower - Mini Helmasaur Room - Left', player) in key_logic.bk_restricted
assert world.get_location('Ganons Tower - Mini Helmasaur Room - Right', player) in key_logic.bk_restricted

View File

@@ -20,7 +20,7 @@ from Main import create_playthrough
__version__ = '0.2-dev'
def main(args):
start_time = time.perf_counter()
start_time = time.process_time()
# initialize the world
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
@@ -86,7 +86,7 @@ def main(args):
world.spoiler.to_file('%s_Spoiler.txt' % outfilebase)
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.perf_counter() - start_time)
logger.debug('Total Time: %s', time.process_time() - start_time)
return world

400
README.md
View File

@@ -32,178 +32,6 @@ Doors are shuffled only within a single dungeon.
Doors are shuffled between dungeons as well.
This mode removes all swords from the itempool. Otherwise just like open.
Special notes:
- The Medallions to open Misery Mire and Turtle Rock can be used without a sword if you stand on the symbol.
- The curtains in Skull Woods and Hyrule Castle Tower that normally require a sword to cut have been removed.
- Ganon takes damage from the Hammer.
- The magic barrier to Hyrule Castle Tower can be broken with a Hammer.
- The Hammer can be used to activate the Ether and Bombos tablets.
### Inverted
This mode is similar to Open but requires the Moon Pearl in order to not transform into a bunny in the Light World and the Sanctuary spawn point is moved to the Dark Sanctuary
Special Notes:
- Link's House is shuffled freely. The Dark Sanctuary is shuffled within West Dark World.
- There are a number of overworld changes to account for the inability to mirror from the Light World to the Dark World.
- Legacy shuffles are not implemente for this mode.
## Game Logic
This determines the Item Requirements for each location.
### No Glitches
The game can be completed without knowing how to perform glitches of any kind.
### Minor Glitches
May require Fake Flippers, Bunny Revival.
### No Logic
Items are placed without regard for progression or the seed being possible. Major glitches are likely required.
## Game Goal
### Ganon
Standard game completion requiring you to collect the 7 crystals, defeat Agahnim 2 and then beat Ganon.
### Pedestal
Places the Triforce at the Master Sword Pedestal. Ganon cannot be damaged.
### All Dungeons
Ganon cannot be damaged until all dungeons (including Hyrule Castle Tower and Ganons Tower) are cleared.
### Triforce Hunt
Triforce Pieces are added to the item pool, and some number of them being found will trigger game completion. Ganon cannot be damaged.
By default 30 Triforce Pieces are placed while 20 are needed to beat the game. Both values can be adjusted with the custom item pool feature.
### Crystals
Standard game completion requiring you to collect the 7 crystals and then beat Ganon.
This is only noticeably different if the the Ganon shuffle option is enabled.
## Game Difficulty
### Normal
This is the default setting that has an item pool most similar to the original
The Legend of Zelda: A Link to the Past.
### Hard
This setting reduces the availability of a variety of minor helpful items, most notably
limiting the player to two bottles, a Tempered Sword, and Blue Mail. Several minor game
mechanics are adjusted to increase difficulty, most notably weakening potions and preventing
the player from having fairies in bottles.
### Expert
This setting is a more extreme version of the Hard setting. Potions are further nerfed, the item
pool is less helpful, and the player can find no armor, only a Master Sword, and only a single bottle.
## Timer Setting
### None
Does not invoke a timer.
### Display
Displays a timer on-screen but does not alter the item pool.
This will prevent the dungeon item count feature in Easy and Compass shuffle from working.
### Timed
Displays a count-up timer on screen that can be reduced with Green Clocks and Blue Clocks or
increased with Red Clocks found in chests that will be added to the itempool.
### Timed-OHKO
Displays a countdown timer on screen that, when it hits zero, will put the player into a one hit
knockout state until more time is added to the clock via some of the Green Clocks that will be added
to the itempool.
### OHKO
The player will be in a one hit knockout state the entire game. This is the same as Timed-OHKO except
without the Clock items and the timer permanently at zero.
### Timed-countdown
Displays a countdown timer on screen that can be increased with Green Clocks and Blue Clocks or
decreased with Red Clocks found in chests that will be added to the itempool. The goal of this mode
is to finish the game without the timer reaching zero, but the game will continue uninterrupted if
the player runs out of time.
## Progressive equipment
Determines if Sword, Shield, and gloves are progressive (upgrading in sequence) or not.
### On (Default)
This setting makes swords, shields, armor, and gloves progressive. The first of any type of equipment found
by the player will be the lowest level item, and each subsequent find of a category will upgrade that type of
equipment.
### Off
This setting makes swords, shields, armor, and gloves non-progressive. All of the items of these types will be
randomly placed in chests, and the player could find them in any order and thus instantly receive high level equipment.
Downgrades are not possible; finding a lower level piece of equipment than what is already in the player's possession
will simply do nothing.
### Random
This setting makes swords, shields, armor, and gloves randomly either progressive or not. Each category is independently
randomized.
## Item Distribution Algorithm
Determines how the items are shuffled.
### Balanced
This is a variation of VT26 that aims to strike a balance between the overworld heavy VT25 and the dungeon heavy VT26 algorithm.
It does this by reshuffling the remaining locations after placing dungeon items.
### VT26
Items and locations are shuffled like in VT25, and dungeon items are now placed using the same algorithm. When Ganon is not
shuffled it includes a slight deliberate bias against having too many desireable items in Ganon's Tower to help counterbalance
the sheer number of chests in that single location.
### VT25
Items and locations are shuffled and placed from the top of the lists. The only thing preventing an item from being placed into a spot
is if is absolutely impossible to be there given the previous made placement choices. Leads to very uniform but guaranteed solvable distributions.
### VT22
The ordinary VT v8.22 algorithm. Fixes issues in placement in VT21 by discarding all previously skipped and unfilled locations
after 2/3 of the progression items were placed to prevent stale late game locations from soaking up the same items all the time.
### VT21
The ordinary VT v8.21 algorithm. Unbiased placement of items into unlocked locations, placing items that unlock new locations first.
May lead to distributions that seem a bit wonky (high likelyhood of ice rod in Turtle Rock, for instance)
### Flood
Pushes out items starting from Link's House and is slightly biased to placing progression items with less restrictions. Use for relatively simple distributions.
### Freshness
Alternative approach to VT22 to improve on VT21 flaws. Locations that are skipped because they are currently unreachable increase in
staleness, decreasing the likelihood of receiving a progress item.
## Entrance Shuffle Algorithm
Determines how locations are shuffled. In all modes other than Insanity and the similar legacy versions, holes shuffle as a pair with the connecting cave and the front
two sections of Skull Woods remain confined to the general Skull Woods area. Link's house is never shuffled as a design decision.
### Vanilla
Doors are not shuffled.
@@ -212,56 +40,6 @@ Doors are not shuffled.
Used for development testing. This will be removed in a future version. Use at your own risk. Might play like a plando.
### Restricted
Uses dungeon shuffling from Simple but freely connects remaining entrances. Caves and dungeons with multiple entrances will be confined to one world.
### Full
Mixes cave and dungeon entrances freely. Caves and dungeons with multiple entrances will be confined to one world.
### Crossed
Mixes cave and dungeon entrances freely, but now connector caves and dungeons can link Light World and Dark World.
### Insanity
Decouples entrances and exits from each other and shuffles them freely. Caves that were single entrance in vanilla still can only exit to the same location from which they were entered.
### Legacy Variants
Similar to the base shuffles, but the distinction between single entrance and multi-entrance caves from older versions of the randomizer is maintained.
Madness_Legacy is the more similar to the modern Insanity. Insanity_Legacy has fake worlds and guaranteed Moon Pearl and Magic Mirror for a very different experience.
### Dungeon Variants
The dungeon variants only mix up dungeons and keep the rest of the overworld vanilla.
## Heartbeep Sound Rate
Select frequency of beeps when on low health. Can completely disable them.
## Heart Color
Select the color of Link's hearts.
## Menu Speed
A setting that lets the player set the rate at which the menu opens and closes.
## Create Spoiler Log
Output a Spoiler File.
## Do not Create Patched Rom
If set, will not produce a patched rom as output. Useful in conjunction with the spoiler log option to batch
generate spoilers for statistical analysis.
## Enable L/R button quickswapping
Use to enable quick item swap with L/R buttons. Press L and R together to switch the state of items like the Mushroom/Powder pair.
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.
@@ -280,26 +58,6 @@ Container and a Bottle being filled with Blue Potion, and one of the four swords
well. The five caves that are removed for these will be randomly selected single entrance caves that did not contain any items or any shops.
In further concert with the Bow changes, all arrows under pots, in chests, and elsewhere in the seed will be replaced with rupees.
## Place Dungeon Items
If not set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
This may lead to different amount of itempool items being placed in a dungeon than you are used to.
## Include Ganon's Tower and Pyramid Hole in Shuffle pool
If set, Ganon's Tower is included in the dungeon shuffle pool and the Pyramid Hole/Exit pair is included in the Holes shuffle pool. Ganon can not be defeated until the primary goal is fulfilled.
This setting removes any bias against Ganon's Tower that some algorithms may have.
## Include Helpful Hints
If set, the 15 telepathic tiles and 5 storytellers scattered about Hyrule will give helpful hints about various items and entrances. An exact breakdown of the hint
distribution is provided as an included text file.
## Use Custom Item Pool
If set, the item pool normally associated with your difficulty setting is replaced by the item pool specified in the custom tab. This feature is only supported when the randomizer is run
via the GUI; attempting to set this via the command line does nothing.
## Seed
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
@@ -321,161 +79,3 @@ Show the help message and exit.
```
For specifying the door shuffle you want as above. (default: basic)
Output a Spoiler File (default: False)
```
--logic [{noglitches,minorglitches,nologic}]
```
Select the game logic (default: noglitches)
```
--mode [{standard,open,swordless,inverted}]
```
Select the game mode. (default: open)
```
--goal [{ganon,pedestal,dungeons,triforcehunt,crystals}]
```
Select the game completion goal. (default: ganon)
```
--difficulty [{normal,hard,expert}]
```
Select the game difficulty. Affects available itempool. (default: normal)
```
--item_functionality [{normal,hard,expert}]
```
Select limits on item functionality to increase difficulty. (default: normal)
```
--timer [{none,display,timed,timed-ohko,ohko,timed-countdown}]
```
Select the timer setting. (default: none)
```
--progressive [{on,off,random}]
```
Select the setting for progressive equipment. (default: on)
```
--algorithm [{freshness,flood,vt21,vt22,vt25,vt26,balanced}]
```
Select item distribution algorithm. (default: balanced)
```
--shuffle [{default,simple,restricted,full,crossed,insanity,restricted_legacy,full_legacy,madness_legacy,insanity_legacy,dungeonsfull,dungeonssimple}]
```
Select entrance shuffle algorithm. (default: full)
```
--rom ROM
```
Path to a Japanese 1.0 A Link to the Past Rom. (default: Zelda no Densetsu - Kamigami no Triforce (Japan).sfc)
```
--loglevel [{error,info,warning,debug}]
```
Select level of logging for output. (default: info)
```
--seed SEED
```
Define seed number to generate. (default: None)
```
--count COUNT
```
Set the count option (default: None)
```
--quickswap
```
Use to enable quick item swap with L/R buttons. (default: False)
```
--fastmenu [{normal,instant,double,triple,quadruple,half}]
```
Alters the rate at which the menu opens and closes. (default: normal)
```
--disablemusic
```
Disables game music, resulting in the game sound being just the SFX. (default: False)
```
--mapshuffle --compassshuffle --keyshuffle --bigkeyshuffle
```
Respectively enable Map/Compass/SmallKey/BigKey shuffle (default: False)
```
--retro
```
Enable Retro mode (default: False)
```
--heartbeep [{normal,half,quarter,off}]
```
Select frequency of beeps when on low health. (default: normal)
```
--heartcolor [{red,blue,green,yellow,random}]
```
Select the color of Link\'s heart meter. (default: red)
```
--sprite SPRITE
```
Use to select a different sprite sheet to use for Link. Path to a binary file of length 0x7000 containing the sprite data stored at address 0x80000 in the rom. (default: None)
```
--accessibility [{items,locations,none}]
```
Sets the item/location accessibility rules. (default: items)
```
--hints
```
Enables helpful hints from storytellers and telepathic tiles (default: False)
```
--no-shuffleganon
```
Disables the "Include Ganon's Tower and Pyramid Hole in Shuffle pool" option. (default: Enabled)
```
--suppress_rom
```
Enables the "Do not Create Patched Rom" option. (default: False)
```
--gui
```
Open the graphical user interface. Preloads selections with set command line parameters.

71
Rom.py
View File

@@ -10,17 +10,18 @@ import sys
import subprocess
from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType
from DoorShuffle import compass_data
from Dungeons import dungeon_music_addresses
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
from Items import ItemFactory
from Items import ItemFactory, item_table
from EntranceShuffle import door_addresses, exit_ids
JAP10HASH = '03a63945398191337e896e5771f77173'
# RANDOMIZERBASEHASH = '1907d4caccffe60fc69940cfa11b2dab'
RANDOMIZERBASEHASH = 'a5ae693acf449264533429d4dbd217e4'
class JsonRom(object):
@@ -68,6 +69,7 @@ class JsonRom(object):
h.update(json.dumps([self.patches]).encode('utf-8'))
return h.hexdigest()
class LocalRom(object):
def __init__(self, file, patch=True):
@@ -109,10 +111,10 @@ class LocalRom(object):
self.write_bytes(int(baseaddress), values)
# verify md5
# patchedmd5 = hashlib.md5()
# patchedmd5.update(self.buffer)
# if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
# raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
patchedmd5 = hashlib.md5()
patchedmd5.update(self.buffer)
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def merge_enemizer_patches(self, patches):
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
@@ -583,6 +585,10 @@ def patch_rom(world, player, rom, enemized):
rom.write_byte(0x151f1, 2)
rom.write_byte(0x15270, 2)
rom.write_byte(0x1597b, 2)
if compass_code_good(rom):
update_compasses(rom, world, player)
else:
logging.getLogger('').warning('Randomizer rom update! Compasses in crossed are borken')
for door in world.doors:
if door.dest is not None and door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs]:
rom.write_bytes(door.getAddress(), door.dest.getTarget(door.toggle))
@@ -2059,6 +2065,59 @@ def patch_shuffled_dark_sanc(world, rom, player):
write_int16s(rom, 0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x])
rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00])
# 24AE17 and 20B7B9
compass_r_addr = 0x122e17 # a9 90 24 8f 9a c7 7e
compass_w_addr = 0x1037b9 # e2 20 ad 0c 04 c9 00 d0
def compass_code_good(rom):
if isinstance(rom, LocalRom):
# a990248f9ac77e
if rom.buffer[compass_r_addr] != 0xa9 or rom.buffer[compass_r_addr+1] != 0x90 or rom.buffer[compass_r_addr+2] != 0x24:
return False
if rom.buffer[compass_r_addr+3] != 0x8f or rom.buffer[compass_r_addr+4] != 0x9a or rom.buffer[compass_r_addr+5] != 0xc7:
return False
if rom.buffer[compass_w_addr] != 0xe2 or rom.buffer[compass_w_addr+1] != 0x20 or rom.buffer[compass_w_addr+2] != 0xad:
return False
if rom.buffer[compass_w_addr+3] != 0x0c or rom.buffer[compass_w_addr+4] != 0x04 or rom.buffer[compass_w_addr+5] != 0xc9:
return False
return True
def update_compasses(rom, world, player):
layouts = world.dungeon_layouts[player]
for name, builder in layouts.items():
digit_offset, sram_byte, write_offset, jmp_nop_flag = compass_data[name]
digit1 = builder.location_cnt // 10
digit2 = builder.location_cnt % 10
rom.write_byte(compass_r_addr+digit_offset, 0x90+digit1)
rom.write_byte(compass_r_addr+digit_offset+7, 0x90+digit2)
# read compass count code
start_address = compass_r_addr+digit_offset+15
# lda $7ef4(sb); jmp $adbe (or 0x122dbe is the next instruction 2dbe=bead)
rom.write_bytes(start_address, [0xaf, sram_byte, 0xf4, 0x7e, 0x4c, 0xbe, 0xad])
# write compass count code
write_address = compass_w_addr+write_offset
# lda $7ef4(sb); inc; sta $7ef4(sb)
rom.write_bytes(write_address, [0xaf, sram_byte, 0xf4, 0x7e, 0x1a, 0x8f, sram_byte, 0xf4, 0x7e])
if jmp_nop_flag == 0:
rom.write_bytes(write_address+9, [0x4c, 0x3f, 0xb9]) # jmp $b93f (or 0x10393f is the next instruction 393f=3fb9)
else:
for i in range(0, jmp_nop_flag):
rom.write_byte(write_address+9+i, 0xea) # nop
InconvenientEntrances = {'Turtle Rock': 'Turtle Rock Main',
'Misery Mire': 'Misery Mire',
'Ice Palace': 'Ice Palace',
'Skull Woods Final Section': 'The back of Skull Woods',
'Death Mountain Return Cave (West)': 'The SW DM foothills cave',
'Mimic Cave': 'Mimic Ledge',
'Dark World Hammer Peg Cave': 'The rows of pegs',
'Pyramid Fairy': 'The crack on the pyramid'
}
InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main',
'Misery Mire': 'Misery Mire',
'Ice Palace': 'Ice Palace',

View File

@@ -1511,13 +1511,13 @@ def create_key_rule_allow_small(small_key_name, player, keys, location):
def create_key_rule_bk_exception(small_key_name, big_key_name, player, keys, bk_keys, bk_locs):
chest_names = [x.name for x in bk_locs]
return lambda state: state.has_key(small_key_name, player, keys) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
return lambda state: (state.has_key(small_key_name, player, keys) and not item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names)))) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
def create_key_rule_bk_exception_or_allow(small_key_name, big_key_name, player, keys, location, bk_keys, bk_locs):
loc = location.name
chest_names = [x.name for x in bk_locs]
return lambda state: state.has_key(small_key_name, player, keys) or (item_name(state, loc, player) in [(small_key_name, player)] and state.has_key(small_key_name, player, keys-1)) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
return lambda state: (state.has_key(small_key_name, player, keys) and not item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names)))) or (item_name(state, loc, player) in [(small_key_name, player)] and state.has_key(small_key_name, player, keys-1)) or (item_in_locations(state, big_key_name, player, zip(chest_names, [player] * len(chest_names))) and state.has_key(small_key_name, player, bk_keys))
def create_advanced_key_rule(key_logic, player, rule):

View File

@@ -228,4 +228,6 @@ def print_wiki_doors(d_regions, world, player):
if __name__ == '__main__':
read_entrance_data(old_rom='C:\\Users\\Randall\\Documents\\kwyn\\orig\\z3.sfc')
pass
# make_new_base2current()
# read_entrance_data(old_rom='')

File diff suppressed because one or more lines are too long