Version bump 0.2.8.0

This commit is contained in:
codemann8
2022-07-12 02:03:32 -05:00
65 changed files with 4159 additions and 1448 deletions
+1
View File
@@ -37,6 +37,7 @@ def main():
parser.add_argument('--uw_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('--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('--shuffle_sfx', help='Shuffles sound sfx', action='store_true')
parser.add_argument('--msu_resume', help='Enable MSU resume', action='store_true')
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes, binary format and have a length of 0x7000 (28672) bytes,
+44 -1
View File
@@ -2,8 +2,15 @@ import os
import time import time
import logging import logging
try:
import bps.apply
import bps.io
except ImportError:
raise Exception('Could not load BPS module')
from Utils import output_path from Utils import output_path
from Rom import LocalRom, apply_rom_settings from Rom import LocalRom, apply_rom_settings
from source.tools.BPS import bps_read_vlv
def adjust(args): def adjust(args):
@@ -25,7 +32,8 @@ def adjust(args):
args.sprite = None args.sprite = None
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, 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.shuffle_sfx) args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing, args.shuffle_sfx,
args.msu_resume)
output_path.cached_path = args.outputpath output_path.cached_path = args.outputpath
rom.write_to_file(output_path('%s.sfc' % outfilebase)) rom.write_to_file(output_path('%s.sfc' % outfilebase))
@@ -34,3 +42,38 @@ def adjust(args):
logger.debug('Total Time: %s', time.process_time() - start) logger.debug('Total Time: %s', time.process_time() - start)
return args return args
def patch(args):
start = time.process_time()
logger = logging.getLogger('')
logger.info('Patching ROM.')
outfile_base = os.path.basename(args.patch)[:-4]
rom = LocalRom(args.baserom, False)
if os.path.isfile(args.baserom):
rom.verify_base_rom()
orig_buffer = rom.buffer.copy()
with open(args.patch, 'rb') as stream:
stream.seek(4) # skip BPS1
bps_read_vlv(stream) # skip source size
target_length = bps_read_vlv(stream)
rom.buffer.extend(bytearray([0x00] * (target_length - len(rom.buffer))))
stream.seek(0)
bps.apply.apply_to_bytearrays(bps.io.read_bps(stream), orig_buffer, rom.buffer)
if not hasattr(args, "sprite"):
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.shuffle_sfx,
args.msu_resume)
output_path.cached_path = args.outputpath
rom.write_to_file(output_path('%s.sfc' % outfile_base))
logger.info('Done. Enjoy.')
logger.debug('Total Time: %s', time.process_time() - start)
return args
+168 -59
View File
@@ -15,6 +15,7 @@ from source.classes.BabelFish import BabelFish
from Utils import int16_as_bytes from Utils import int16_as_bytes
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
from RoomData import Room from RoomData import Room
from source.dungeon.RoomObject import RoomObject
class World(object): class World(object):
@@ -62,7 +63,6 @@ class World(object):
self.lock_aga_door_in_escape = False self.lock_aga_door_in_escape = False
self.save_and_quit_from_boss = True self.save_and_quit_from_boss = True
self.accessibility = accessibility.copy() self.accessibility = accessibility.copy()
self.initial_overworld_flags = {}
self.fix_skullwoods_exit = {} self.fix_skullwoods_exit = {}
self.fix_palaceofdarkness_exit = {} self.fix_palaceofdarkness_exit = {}
self.fix_trock_exit = {} self.fix_trock_exit = {}
@@ -99,6 +99,7 @@ class World(object):
self._portal_cache = {} self._portal_cache = {}
self.sanc_portal = {} self.sanc_portal = {}
self.fish = BabelFish() self.fish = BabelFish()
self.pot_contents = {}
for player in range(1, players + 1): for player in range(1, players + 1):
# If World State is Retro, set to Open and set Retro flag # If World State is Retro, set to Open and set Retro flag
@@ -120,7 +121,6 @@ class World(object):
set_player_attr('ganon_at_pyramid', True) set_player_attr('ganon_at_pyramid', True)
set_player_attr('ganonstower_vanilla', True) set_player_attr('ganonstower_vanilla', True)
set_player_attr('sewer_light_cone', self.mode[player] == 'standard') set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
set_player_attr('initial_overworld_flags', [0] * 0x80)
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.is_tile_swapped(0x05, player)) set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.is_tile_swapped(0x05, 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_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_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
@@ -154,9 +154,11 @@ class World(object):
set_player_attr('potshuffle', False) set_player_attr('potshuffle', False)
set_player_attr('pot_contents', None) set_player_attr('pot_contents', None)
set_player_attr('pseudoboots', False) set_player_attr('pseudoboots', False)
set_player_attr('collection_rate', False)
set_player_attr('colorizepots', False)
set_player_attr('pot_pool', {})
set_player_attr('shopsanity', False) set_player_attr('shopsanity', False)
set_player_attr('keydropshuffle', False)
set_player_attr('mixed_travel', 'prevent') set_player_attr('mixed_travel', 'prevent')
set_player_attr('standardize_palettes', 'standardize') set_player_attr('standardize_palettes', 'standardize')
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
@@ -456,6 +458,8 @@ class World(object):
location.item = item location.item = item
item.location = location item.location = location
item.world = self item.world = self
if location.player != item.player and location.type == LocationType.Pot:
self.pot_contents[location.player].multiworld_count += 1
if collect: if collect:
self.state.collect(item, location.event, location) self.state.collect(item, location.event, location)
@@ -1361,6 +1365,8 @@ class CollectionState(object):
return self.has('Fire Rod', player) or self.has('Lamp', player) return self.has('Fire Rod', player) or self.has('Lamp', player)
def can_flute(self, player): def can_flute(self, player):
if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player):
return False
if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)): if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)):
return True return True
lw = self.world.get_region('Kakariko Area', player) lw = self.world.get_region('Kakariko Area', player)
@@ -2632,10 +2638,19 @@ class Location(object):
self.item_rule = lambda item: True self.item_rule = lambda item: True
self.player = player self.player = player
self.skip = False self.skip = False
self.type = LocationType.Normal if not crystal else LocationType.Prize
self.pot = None
def can_fill(self, state, item, check_access=True): def can_fill(self, state, item, check_access=True):
if not self.valid_multiworld(state, item):
return False
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))) 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 valid_multiworld(self, state, item):
if self.type == LocationType.Pot and self.player != item.player:
return state.world.pot_contents[self.player].multiworld_count < 256
return True
def can_reach(self, state): def can_reach(self, state):
return self.parent_region.can_reach(state) and self.access_rule(state) return self.parent_region.can_reach(state) and self.access_rule(state)
@@ -2671,6 +2686,15 @@ class Location(object):
return hash((self.name, self.player)) return hash((self.name, self.player))
class LocationType(FastEnum):
Normal = 0
Prize = 1
Logical = 2
Shop = 3
Pot = 4
Drop = 5
class Item(object): class Item(object):
def __init__(self, name='', advancement=False, priority=False, type=None, code=None, price=999, pedestal_hint=None, def __init__(self, name='', advancement=False, priority=False, type=None, code=None, price=999, pedestal_hint=None,
@@ -2907,17 +2931,19 @@ class Spoiler(object):
'enemy_shuffle': self.world.enemy_shuffle, 'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health, 'enemy_health': self.world.enemy_health,
'enemy_damage': self.world.enemy_damage, 'enemy_damage': self.world.enemy_damage,
'potshuffle': self.world.potshuffle,
'players': self.world.players, 'players': self.world.players,
'teams': self.world.teams, 'teams': self.world.teams,
'experimental': self.world.experimental, 'experimental': self.world.experimental,
'keydropshuffle': self.world.keydropshuffle, 'dropshuffle': self.world.dropshuffle,
'pottery': self.world.pottery,
'potshuffle': self.world.potshuffle,
'shopsanity': self.world.shopsanity, 'shopsanity': self.world.shopsanity,
'pseudoboots': self.world.pseudoboots, 'pseudoboots': self.world.pseudoboots,
'triforcegoal': self.world.treasure_hunt_count, 'triforcegoal': self.world.treasure_hunt_count,
'triforcepool': self.world.treasure_hunt_total, 'triforcepool': self.world.treasure_hunt_total,
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}
} }
for p in range(1, self.world.players + 1): for p in range(1, self.world.players + 1):
from ItemList import set_default_triforce from ItemList import set_default_triforce
if self.world.custom and p in self.world.customitemarray: if self.world.custom and p in self.world.customitemarray:
@@ -2945,7 +2971,7 @@ class Spoiler(object):
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0] self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0]
self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1]
self.locations = OrderedDict() self.locations = OrderedDict()
listed_locations = set() listed_locations = set()
@@ -2962,11 +2988,11 @@ class Spoiler(object):
listed_locations.update(cave_locations) listed_locations.update(cave_locations)
for dungeon in self.world.dungeons: for dungeon in self.world.dungeons:
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item] dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip]
self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations]) self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
listed_locations.update(dungeon_locations) listed_locations.update(dungeon_locations)
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations] other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip]
if other_locations: if other_locations:
self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations]) self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
listed_locations.update(other_locations) listed_locations.update(other_locations)
@@ -3041,6 +3067,18 @@ class Spoiler(object):
return json.dumps(out) return json.dumps(out)
def mystery_meta_to_file(self, filename):
self.parse_meta()
with open(filename, 'w') as outfile:
line_width = 35
outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed))
for k,v in self.metadata["versions"].items():
outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v)
for player in range(1, self.world.players + 1):
if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player])
def meta_to_file(self, filename): def meta_to_file(self, filename):
def yn(flag): def yn(flag):
return 'Yes' if flag else 'No' return 'Yes' if flag else 'No'
@@ -3048,7 +3086,7 @@ class Spoiler(object):
self.parse_meta() self.parse_meta()
with open(filename, 'w') as outfile: with open(filename, 'w') as outfile:
line_width = 35 line_width = 35
outfile.write('ALttP Entrance Randomizer - Seed: %s\n\n' % (self.world.seed)) outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed))
for k,v in self.metadata["versions"].items(): for k,v in self.metadata["versions"].items():
outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v) outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v)
outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm) outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm)
@@ -3088,15 +3126,15 @@ class Spoiler(object):
outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player]))
if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]: if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]:
outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player])
if self.metadata['goal'][player] != 'trinity': outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player])
outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player])
outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player])
if self.metadata['door_shuffle'][player] != 'vanilla': if self.metadata['door_shuffle'][player] != 'vanilla':
outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player])
outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player]))
outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player])
outfile.write('Pot Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player])) outfile.write('Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['dropshuffle'][player]))
outfile.write('Key Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['keydropshuffle'][player])) outfile.write('Pottery Mode:'.ljust(line_width) + '%s\n' % self.metadata['pottery'][player])
outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player]))
outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player]))
outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player]))
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['keyshuffle'][player])) outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['keyshuffle'][player]))
@@ -3111,19 +3149,37 @@ class Spoiler(object):
outfile.write('Starting Inventory:'.ljust(line_width)) outfile.write('Starting Inventory:'.ljust(line_width))
outfile.write('\n'.ljust(line_width+1).join(self.startinventory) + '\n') outfile.write('\n'.ljust(line_width+1).join(self.startinventory) + '\n')
def hashes_to_file(self, filename):
with open(filename, 'r') as infile:
contents = infile.readlines()
def insert(lines, i, value):
lines.insert(i, value)
i += 1
return i
idx = 2
if self.world.players > 1:
idx = insert(contents, idx, 'Hashes:')
for player in range(1, self.world.players + 1):
if self.world.players > 1:
idx = insert(contents, idx, f'\nPlayer {player}: {self.world.get_player_names(player)}\n')
if len(self.hashes) > 0:
for team in range(self.world.teams):
player_name = self.world.player_names[player][team]
label = f"Hash - {player_name} (Team {team+1}): " if self.world.teams > 1 else 'Hash: '
idx = insert(contents, idx, f'{label}{self.hashes[player, team]}\n')
if self.world.players > 1:
insert(contents, idx, '\n') # return value ignored here, if you want to add more lines
with open(filename, "w") as f:
contents = "".join(contents)
f.write(contents)
def to_file(self, filename): def to_file(self, filename):
self.parse_data() self.parse_data()
with open(filename, 'a') as outfile: with open(filename, 'a') as outfile:
line_width = 35 line_width = 35
if self.world.players > 1:
outfile.write('\nHashes:')
for player in range(1, self.world.players + 1):
if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
if len(self.hashes) > 0:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team]))
outfile.write('\n\nRequirements:\n\n') outfile.write('\n\nRequirements:\n\n')
for dungeon, medallion in self.medallions.items(): for dungeon, medallion in self.medallions.items():
outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion)
@@ -3220,7 +3276,7 @@ class Spoiler(object):
outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') 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']])) outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
def playthru_to_file(self, filename): def playthrough_to_file(self, filename):
with open(filename, 'a') as outfile: with open(filename, 'a') as outfile:
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names # items: Item names
@@ -3318,15 +3374,40 @@ class PotFlags(FastEnum):
Normal = 0x0 Normal = 0x0
NoSwitch = 0x1 # A switch should never go here NoSwitch = 0x1 # A switch should never go here
SwitchLogicChange = 0x2 # A switch can go here, but requires a logic change SwitchLogicChange = 0x2 # A switch can go here, but requires a logic change
Block = 0x4 # This is actually a block
LowerRegion = 0x8 # This is a pot in the lower region
class Pot(object): class Pot(object):
def __init__(self, x, y, item, room, flags = PotFlags.Normal): def __init__(self, x, y, item, room, flags=PotFlags.Normal, obj=None):
self.x = x self.x = x
self.y = y self.y = y
self.item = item self.item = item
self.room = room self.room = room
self.flags = flags self.flags = flags
self.indicator = None # 0x80 for standing item, 0xC0 multiworld item
self.standing_item_code = None # standing item code if nay
self.obj_ref = obj
self.location = None # location back ref
def copy(self):
obj_ref = RoomObject(self.obj_ref.address, self.obj_ref.data) if self.obj_ref else None
return Pot(self.x, self.y, self.item, self.room, self.flags, obj_ref)
def pot_data(self):
high_byte = self.y
if self.flags & PotFlags.LowerRegion:
high_byte |= 0x20
if self.indicator:
high_byte |= self.indicator
item = self.item if not self.indicator else self.standing_item_code
return [self.x, high_byte, item]
def __eq__(self, other):
return self.x == other.x and self.y == other.y and self.room == other.room
def __hash__(self):
return hash((self.x, self.y, self.room))
# byte 0: DDOO OEEE (DR, OR, ER) # byte 0: DDOO OEEE (DR, OR, ER)
@@ -3344,33 +3425,43 @@ goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "cryst
diff_mode = {"normal": 0, "hard": 1, "expert": 2} diff_mode = {"normal": 0, "hard": 1, "expert": 2}
func_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2}
# byte 3: SKMM PIII (shop, keydrop, mixed, palettes, intensity) # byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity)
# keydrop now has it's own byte
mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
# intensity is 3 bits (reserves 4-7 levels) # intensity is 3 bits (reserves 4-7 levels)
# byte 4: CCCC CTTX (crystals gt, ctr2, experimental) # new byte 4: ?DDD PPPP (unused, drop, pottery)
# dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes)
pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7,
'clustered': 8, 'nonempty': 9}
# byte 5: CCCC CTTX (crystals gt, ctr2, experimental)
counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}
# byte 5: CCCC CPAA (crystals ganon, pyramid, access # byte 6: CCCC CPAA (crystals ganon, pyramid, access
access_mode = {"items": 0, "locations": 1, "none": 2} access_mode = {"items": 0, "locations": 1, "none": 2}
# byte 6: BSMC BBEE (big, small, maps, compass, bosses, enemies) # byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies)
boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3} enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3}
enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
# byte 7: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) # byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links)
# potshuffle decprecated, now unused
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
e_dmg = {"default": 0, "shuffled": 1, "random": 2} e_dmg = {"default": 0, "shuffled": 1, "random": 2}
# byte 8: RRAA A??? (restrict boss mode, algorithm, ? = unused) # byte 9: RRAA ABBB (restrict boss mode, algorithm, boss shuffle)
rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2} rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2}
# algorithm: # algorithm:
algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4, 'major_only': 5} algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4, 'major_only': 5}
boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique': 4}
# additions # additions
# psuedoboots does not effect code # psuedoboots does not effect code
# sfx_shuffle and other adjust items does not effect settings code # sfx_shuffle and other adjust items does not effect settings code
# Bump this when making changes that are not backwards compatible (nearly all of them)
settings_version = 0
class Settings(object): class Settings(object):
@@ -3385,10 +3476,12 @@ class Settings(object):
(goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3)
| (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0),
(0x80 if w.shopsanity[p] else 0) | (0x40 if w.keydropshuffle[p] else 0) (0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4)
| (mixed_travel_mode[w.mixed_travel[p]] << 4) | (0x8 if w.standardize_palettes[p] == "original" else 0) | (0x8 if w.standardize_palettes[p] == "original" else 0)
| (0 if w.intensity[p] == "random" else w.intensity[p]), | (0 if w.intensity[p] == "random" else w.intensity[p]),
(0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]),
((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3)
| (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0), | (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0),
@@ -3397,18 +3490,25 @@ class Settings(object):
(0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0)
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
| (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), | (enemy_mode[w.enemy_shuffle[p]]),
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0),
(rb_mode[w.restrict_boss_items[p]] << 6)]) (rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]),
settings_version])
return base64.b64encode(code, "+-".encode()).decode() return base64.b64encode(code, "+-".encode()).decode()
@staticmethod @staticmethod
def adjust_args_from_code(code, player, args): def adjust_args_from_code(code, player, args):
settings, p = base64.b64decode(code.encode(), "+-".encode()), player settings, p = base64.b64decode(code.encode(), "+-".encode()), player
if len(settings) < 11:
raise Exception('Provided code is incompatible with this version')
if settings[10] != settings_version:
raise Exception('Provided code is incompatible with this version')
def r(d): def r(d):
return {y: x for x, y in d.items()} return {y: x for x, y in d.items()}
@@ -3421,36 +3521,45 @@ class Settings(object):
args.difficulty[p] = r(diff_mode)[(settings[2] & 0x18) >> 3] args.difficulty[p] = r(diff_mode)[(settings[2] & 0x18) >> 3]
args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1] args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1]
args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5] args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5]
args.accessibility[p] = r(access_mode)[settings[5] & 0x3] args.accessibility[p] = r(access_mode)[settings[6] & 0x3]
args.retro[p] = True if settings[1] & 0x01 else False args.retro[p] = True if settings[1] & 0x01 else False
args.hints[p] = True if settings[2] & 0x01 else False args.hints[p] = True if settings[2] & 0x01 else False
args.retro[p] = True if settings[1] & 0x01 else False
args.shopsanity[p] = True if settings[3] & 0x80 else False args.shopsanity[p] = True if settings[3] & 0x80 else False
args.keydropshuffle[p] = True if settings[3] & 0x40 else False # args.keydropshuffle[p] = True if settings[3] & 0x40 else False
args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4]
args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize" args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize"
intensity = settings[3] & 0x7 intensity = settings[3] & 0x7
args.intensity[p] = "random" if intensity == 0 else intensity args.intensity[p] = "random" if intensity == 0 else intensity
args.dungeon_counters[p] = r(counter_mode)[(settings[4] & 0x6) >> 1]
cgt = (settings[4] & 0xf8) >> 3 # args.shuffleswitches[p] = True if settings[4] & 0x80 else False
args.dropshuffle[p] = True if settings[4] & 0x10 else False
args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F]
args.dungeon_counters[p] = r(counter_mode)[(settings[5] & 0x6) >> 1]
cgt = (settings[5] & 0xf8) >> 3
args.crystals_gt[p] = "random" if cgt == 8 else cgt args.crystals_gt[p] = "random" if cgt == 8 else cgt
args.experimental[p] = True if settings[4] & 0x1 else False args.experimental[p] = True if settings[5] & 0x1 else False
cgan = (settings[5] & 0xf8) >> 3
cgan = (settings[6] & 0xf8) >> 3
args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.crystals_ganon[p] = "random" if cgan == 8 else cgan
args.openpyramid[p] = True if settings[5] & 0x4 else False args.openpyramid[p] = True if settings[6] & 0x4 else False
args.bigkeyshuffle[p] = True if settings[6] & 0x80 else False
args.keyshuffle[p] = True if settings[6] & 0x40 else False args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False
args.mapshuffle[p] = True if settings[6] & 0x20 else False args.keyshuffle[p] = True if settings[7] & 0x40 else False
args.compassshuffle[p] = True if settings[6] & 0x10 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False
args.shufflebosses[p] = r(boss_mode)[(settings[6] & 0xc) >> 2] args.compassshuffle[p] = True if settings[7] & 0x10 else False
args.shuffleenemies[p] = r(enemy_mode)[settings[6] & 0x3] # args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2]
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3]
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
args.shufflepots[p] = True if settings[7] & 0x4 else False args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5]
args.bombbag[p] = True if settings[7] & 0x2 else False args.enemy_damage[p] = r(e_dmg)[(settings[8] & 0x18) >> 3]
args.shufflelinks[p] = True if settings[7] & 0x1 else False args.shufflepots[p] = True if settings[8] & 0x4 else False
if len(settings) > 8: args.bombbag[p] = True if settings[8] & 0x2 else False
args.restrict_boss_items[p] = True if r(rb_mode)[(settings[8] & 0x80) >> 6] else False args.shufflelinks[p] = True if settings[8] & 0x1 else False
if len(settings) > 9:
args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6]
args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
class KeyRuleType(FastEnum): class KeyRuleType(FastEnum):
+30 -15
View File
@@ -166,18 +166,19 @@ def place_bosses(world, player):
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']] placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
if world.boss_shuffle[player] in ["simple", "full"]: # temporary hack for swordless kholdstare:
# temporary hack for swordless kholdstare: if world.boss_shuffle[player] in ["simple", "full", "unique"]:
if world.swords[player] == 'swordless': if world.swords[player] == 'swordless':
world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player) world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player)
logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace') logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace')
boss_locations.remove(['Ice Palace', None]) boss_locations.remove(['Ice Palace', None])
placeable_bosses.remove('Kholdstare') placeable_bosses.remove('Kholdstare')
if world.boss_shuffle[player] in ["simple", "full"]:
if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random else: # all bosses present, the three duplicates chosen at random
bosses = all_bosses + random.sample(placeable_bosses, 3) bosses = placeable_bosses + random.sample(placeable_bosses, 3)
logging.getLogger('').debug('Bosses chosen %s', bosses) logging.getLogger('').debug('Bosses chosen %s', bosses)
@@ -189,12 +190,7 @@ def place_bosses(world, player):
raise FillError('Could not place boss for location %s' % loc_text) raise FillError('Could not place boss for location %s' % loc_text)
bosses.remove(boss) bosses.remove(boss)
# GT Bosses can move dungeon - find the real dungeon to place them in place_boss(boss, level, loc, loc_text, world, player)
if level:
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
elif world.boss_shuffle[player] == "random": #all bosses chosen at random elif world.boss_shuffle[player] == "random": #all bosses chosen at random
for [loc, level] in boss_locations: for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '') loc_text = loc + (' ('+level+')' if level else '')
@@ -203,9 +199,28 @@ def place_bosses(world, player):
except IndexError: except IndexError:
raise FillError('Could not place boss for location %s' % loc_text) raise FillError('Could not place boss for location %s' % loc_text)
# GT Bosses can move dungeon - find the real dungeon to place them in place_boss(boss, level, loc, loc_text, world, player)
if level: elif world.boss_shuffle[player] == 'unique':
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0] bosses = list(placeable_bosses)
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text) for [loc, level] in boss_locations:
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player) loc_text = loc + (' ('+level+')' if level else '')
try:
if level:
boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)])
else:
boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)])
bosses.remove(boss)
except IndexError:
raise FillError('Could not place boss for location %s' % loc_text)
place_boss(boss, level, loc, loc_text, world, player)
def place_boss(boss, level, loc, loc_text, world, player):
# GT Bosses can move dungeon - find the real dungeon to place them in
if level:
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
+30 -23
View File
@@ -1,8 +1,15 @@
# Changelog # Changelog
### 0.2.8.0
- ~Merged DR v1.0.1.0 - Pottery options, BPS support, MSU Resume, Collection Rate Counter~
- Various improvements to increase generation success rate and reduce generation time
- Fixed issue with playthru recognizing Aga accessibility
- Fixed issue with applying rules correctly to Murahdahla, fixing Murahdahla+Beatable issues
- Fixed issue with Flute+Rainstate, flute use is no longer in logic until Zelda is delivered
### 0.2.7.3 ### 0.2.7.3
- Restructured OWR algorithm to include some additional scenarios not previously allowed - Restructured OWR algorithm to include some additional scenarios not previously allowed
- Added new Inverted D-pad controls for Social Distorion (ie. Mirror Mode) support - Added new Inverted D-pad controls for Social Distortion (ie. Mirror Mode) support
- Crossed OWR/Special OW Areas are now included in the spoiler log - Crossed OWR/Special OW Areas are now included in the spoiler log
- Fixed default TF pieces with Trinity in Mystery - Fixed default TF pieces with Trinity in Mystery
- Added bush crabs to rupee farm logic (only in non-enemizer) - Added bush crabs to rupee farm logic (only in non-enemizer)
@@ -19,7 +26,7 @@
- Added proper branch-specific versioning (ie. Dev branch has '-u' suffixing the version number while Release/Main branch does not) - Added proper branch-specific versioning (ie. Dev branch has '-u' suffixing the version number while Release/Main branch does not)
### 0.2.7.0 ### 0.2.7.0
- ~~Merged DR v1.0.0.3 - MANY changes, major things listed below~~ - ~Merged DR v1.0.0.3 - MANY changes, major things listed below~
- New Item Fills (Districts/Vanilla/Major Location/Dungeon) - New Item Fills (Districts/Vanilla/Major Location/Dungeon)
- New OW Map Prize Indicators (In ER, map checks can spoil dungeon locations with a user setting) - New OW Map Prize Indicators (In ER, map checks can spoil dungeon locations with a user setting)
- Forbidden Boss Items (Exclude certain dungeon items from dropping on bosses) - Forbidden Boss Items (Exclude certain dungeon items from dropping on bosses)
@@ -70,7 +77,7 @@
- Fixed issue with incorrect Mirror bonking - Fixed issue with incorrect Mirror bonking
- Fixed issue with old man follower death to Pyramid - Fixed issue with old man follower death to Pyramid
- Fixed Hera boss music not playing when boss not defeated - Fixed Hera boss music not playing when boss not defeated
- ~~Merged DR v0.5.1.7 - TT boss trap door fix/Applied Glitched flag~~ - ~Merged DR v0.5.1.7 - TT boss trap door fix/Applied Glitched flag~
### 0.2.4.0 ### 0.2.4.0
- Added Guaranteed OWR Reachability - Added Guaranteed OWR Reachability
@@ -96,7 +103,7 @@
- Fake flipper damage fix improved to skip the long delay after the scroll - Fake flipper damage fix improved to skip the long delay after the scroll
- Fixed missing Blue Potion in Lake Shop in Inverted - Fixed missing Blue Potion in Lake Shop in Inverted
- Added legacy OW Crossed option 'None (Allowed)' to support old behavior when invalid option was used in Mystery - Added legacy OW Crossed option 'None (Allowed)' to support old behavior when invalid option was used in Mystery
- ~~Merged DR v0.5.1.6 - Money balancing fix/Boss logic fixes with Bombbag~~ - ~Merged DR v0.5.1.6 - Money balancing fix/Boss logic fixes with Bombbag~
### 0.2.3.3 ### 0.2.3.3
- Added OW Layout validation that reduces the cases where some screens are unreachable - Added OW Layout validation that reduces the cases where some screens are unreachable
@@ -138,7 +145,7 @@
- Fixed music track change to Sanc music when Standard mode is delivering Zelda - Fixed music track change to Sanc music when Standard mode is delivering Zelda
- Fixed SP flooding issue - Fixed SP flooding issue
- Fixed issue with Shuffle Ganon in CLI/GUI - Fixed issue with Shuffle Ganon in CLI/GUI
- ~~Merged DR v0.5.1.5 - Mystery subweights~~ - ~Merged DR v0.5.1.5 - Mystery subweights~
### 0.2.1.2 ### 0.2.1.2
- Fixed issue with whirlpools not changing world when in Crossed OW - Fixed issue with whirlpools not changing world when in Crossed OW
@@ -159,7 +166,7 @@
- Smith deletion on S+Q only occurs if Blacksmith not reachable from starting locations - Smith deletion on S+Q only occurs if Blacksmith not reachable from starting locations
- Spoiler log improvements to prevent spoiling in the beginning 'meta' section - Spoiler log improvements to prevent spoiling in the beginning 'meta' section
- Various minor fixes and improvements - Various minor fixes and improvements
- ~~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~~ - ~Merged DR v0.5.1.4 - ROM bug fixes/keylogic improvements~
### 0.1.9.4 ### 0.1.9.4
- Hotfix for bad 0.1.9.3 version - Hotfix for bad 0.1.9.3 version
@@ -172,11 +179,11 @@
### 0.1.9.2 ### 0.1.9.2
- Fixed spoiler log and mystery for new Crossed/Mixed structure - Fixed spoiler log and mystery for new Crossed/Mixed structure
- Minor preparations and tweaks to ER framework (added global Entrance/Exit pool) - Minor preparations and tweaks to ER framework (added global Entrance/Exit pool)
- ~~Merged DR v0.5.1.2 - Blind Prison shuffled outside TT/Keylogic Improvements~~ - ~Merged DR v0.5.1.2 - Blind Prison shuffled outside TT/Keylogic Improvements~
### 0.1.9.1 ### 0.1.9.1
- Fixed logic issue with leaving IP entrance not requiring flippers - Fixed logic issue with leaving IP entrance not requiring flippers
- ~~Merged DR v0.5.1.1 - Map Indicator Fix/Boss Shuffle Bias/Shop Hints~~ - ~Merged DR v0.5.1.1 - Map Indicator Fix/Boss Shuffle Bias/Shop Hints~
### 0.1.9.0 ### 0.1.9.0
- Expanded Crossed OW to four separate options, see Readme for details - Expanded Crossed OW to four separate options, see Readme for details
@@ -190,7 +197,7 @@
- Fixed issues with Link/Bunny state in Crossed OW - Fixed issues with Link/Bunny state in Crossed OW
- Fixed issue with Standard+Parallel not using vanilla connections for Escape - Fixed issue with Standard+Parallel not using vanilla connections for Escape
- Fixed issue with Mystery for OW boolean options - Fixed issue with Mystery for OW boolean options
- ~~Merged DR v0.5.1.0 - Major Keylogic Update~~ - ~Merged DR v0.5.1.0 - Major Keylogic Update~
### 0.1.8.1 ### 0.1.8.1
- Fixed issue with activating flute in DW (OW Mixed) - Fixed issue with activating flute in DW (OW Mixed)
@@ -205,12 +212,12 @@
- Added OW Shuffle support for Plando module (needs user testing) - 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 Sanc start at TR as bunny when it is LW
- Fixed issue with Pyramid Hole not getting shuffled - Fixed issue with Pyramid Hole not getting shuffled
- ~~Merged DR v0.5.0.3 - Minor DR fixes~~ - ~Merged DR v0.5.0.3 - Minor DR fixes~
### 0.1.7.4 ### 0.1.7.4
- Fixed issue with Mixed OW failing to generate when HC/Pyramid is swapped - Fixed issue with Mixed OW failing to generate when HC/Pyramid is swapped
- Various fixes to improve generation rates for Mixed OW Shuffle - Various fixes to improve generation rates for Mixed OW Shuffle
- ~~Merged DR v0.5.0.2 - Shuffle SFX~~ - ~Merged DR v0.5.0.2 - Shuffle SFX~
### 0.1.7.3 ### 0.1.7.3
- Fixed minor issue with ambient SFX stopping and starting on OW screen load - Fixed minor issue with ambient SFX stopping and starting on OW screen load
@@ -230,10 +237,10 @@
### 0.1.7.0 ### 0.1.7.0
- Expanded new DR bomb logic to all modes (bomb usage in logic only if there is an unlimited supply of bombs available) - Expanded new DR bomb logic to all modes (bomb usage in logic only if there is an unlimited supply of bombs available)
- ~~Merged DR v0.5.0.1 - Bombbag mode / Enemizer fixes~~ - ~Merged DR v0.5.0.1 - Bombbag mode / Enemizer fixes~
### 0.1.6.9 ### 0.1.6.9
- ~~Merged DR v0.4.0.12 - Secure random update / Credits fix~~ - ~Merged DR v0.4.0.12 - Secure random update / Credits fix~
### 0.1.6.8 ### 0.1.6.8
- Implemented a smarter Balanced Flute Shuffle algorithm - Implemented a smarter Balanced Flute Shuffle algorithm
@@ -247,14 +254,14 @@
- Fixed Boss Music when boss room is entered thru straight stairs - Fixed Boss Music when boss room is entered thru straight stairs
- Suppressed in-dungeon music changes when DR is enabled - Suppressed in-dungeon music changes when DR is enabled
- Fixed issue with Pyramid Exit exiting to wrong location in ER - Fixed issue with Pyramid Exit exiting to wrong location in ER
- ~~Merged DR v0.4.0.11 - Various DR changes~~ - ~Merged DR v0.4.0.11 - Various DR changes~
### 0.1.6.6 ### 0.1.6.6
- ~~Merged DR v0.4.0.9 - P/C Indicator / Credits fix / CLI Hints Fix~~ - ~Merged DR v0.4.0.9 - P/C Indicator / Credits fix / CLI Hints Fix~
### 0.1.6.5 ### 0.1.6.5
- Reduced chance of diagonal flute spot in Balanced - Reduced chance of diagonal flute spot in Balanced
- ~~Merged DR v0.4.0.8 - Boss Indicator / Psuedo Boots / Quickswap Update / Credits Updates~~ - ~Merged DR v0.4.0.8 - Boss Indicator / Psuedo Boots / Quickswap Update / Credits Updates~
### 0.1.6.4 ### 0.1.6.4
- Fixed Frogsmith and Stumpy and restored progression in these locations - Fixed Frogsmith and Stumpy and restored progression in these locations
@@ -293,15 +300,15 @@
### 0.1.5.0 ### 0.1.5.0
- Added OW Tile Swap setting - Added OW Tile Swap setting
- Fixed horizontal VRAM visual loading glitch on megatiles - Fixed horizontal VRAM visual loading glitch on megatiles
- ~~Merged DR v0.4.0.7 - Fast Credits / Reduced Flashing / Sprite Author in Credits~~ - ~~Merged DR v0.4.0.7 - Fast Credits / Reduced Flashing / Sprite Author in Credits~~ Didn't fully merge
### 0.1.4.3 ### 0.1.4.3
- Merged DR v0.4.0.6 - TT Maiden Attic Hint / DR Entrance Floor Mat Mods / Hard/Expert Item Pool Fix - ~Merged DR v0.4.0.6 - TT Maiden Attic Hint / DR Entrance Floor Mat Mods / Hard/Expert Item Pool Fix~
### 0.1.4.2 ### 0.1.4.2
- Modified various OW map terrain specific to OW Shuffle - Modified various OW map terrain specific to OW Shuffle
- Changed World check to table-based vs OW ID-based (should have no effect with current modes) - Changed World check to table-based vs OW ID-based (should have no effect with current modes)
- Merged DR v0.4.0.5 - Mystery Boss Shuffle Fix / Swordless+Hard Item Pool Fix / Insanity+Inverted ER Fixes - ~Merged DR v0.4.0.5 - Mystery Boss Shuffle Fix / Swordless+Hard Item Pool Fix / Insanity+Inverted ER Fixes~
### 0.1.4.1 ### 0.1.4.1
- Moved Inverted Pyramid Entrance to top of HC Ledge - Moved Inverted Pyramid Entrance to top of HC Ledge
@@ -315,11 +322,11 @@
- Various logic fixes and region prep for Inverted - Various logic fixes and region prep for Inverted
- Fixed muted MSU-1 music in door rando when descending GT Climb stairs - Fixed muted MSU-1 music in door rando when descending GT Climb stairs
- Fixed Standard + Vanilla (thanks compiling) - Fixed Standard + Vanilla (thanks compiling)
- Merged DR v0.4.0.4 - Shuffle Link's House / Experimental Bunny Start / 10 Bomb Fix - ~Merged DR v0.4.0.4 - Shuffle Link's House / Experimental Bunny Start / 10 Bomb Fix~
### 0.1.3.0 ### 0.1.3.0
- Added OWG Logic for OW Shuffle - Added OWG Logic for OW Shuffle
- Merged DR v0.4.0.2 - OWG Framework / YAML - ~Merged DR v0.4.0.2 - OWG Framework / YAML~
### 0.1.2.2 ### 0.1.2.2
- Re-purposed OW Shuffle setting to Layout Shuffle - Re-purposed OW Shuffle setting to Layout Shuffle
@@ -328,7 +335,7 @@
### 0.1.2.1 ### 0.1.2.1
- Made possible fix for Standard - Made possible fix for Standard
- Merged DR v0.3.1.10 - Fixed Standard generation - ~Merged DR v0.3.1.10 - Fixed Standard generation~
### 0.1.2.0 ### 0.1.2.0
- Added 'Parallel Worlds' toggle option - Added 'Parallel Worlds' toggle option
@@ -338,7 +345,7 @@
### 0.1.1.2 ### 0.1.1.2
- If Link's current position fits within the incoming gap, Link will not get re-centered to the incoming gap - If Link's current position fits within the incoming gap, Link will not get re-centered to the incoming gap
- Added Rule for Pearl required to drop down back of SW - Added Rule for Pearl required to drop down back of SW
- Merged DR v0.3.1.8 - Improved Shopsanity pricing - Fixed Retro generation - ~Merged DR v0.3.1.8 - Improved Shopsanity pricing - Fixed Retro generation~
### 0.1.1.1 ### 0.1.1.1
- Fixed camera unlocking issue - Fixed camera unlocking issue
+20 -6
View File
@@ -88,6 +88,10 @@ def parse_cli(argv, no_defaults=False):
if ret.keysanity: if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4 ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
if ret.keydropshuffle:
ret.dropshuffle = True
ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery
if multiargs.multi: if multiargs.multi:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
@@ -97,14 +101,15 @@ def parse_cli(argv, no_defaults=False):
'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'ow_shuffle', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
'reduce_flashing', 'shuffle_sfx']: 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
'msu_resume', 'collection_rate', 'colorizepots']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})
@@ -141,6 +146,8 @@ def parse_settings():
"progressive": "on", "progressive": "on",
"accessibility": "items", "accessibility": "items",
"algorithm": "balanced", "algorithm": "balanced",
'mystery': False,
'suppress_meta': False,
"restrict_boss_items": "none", "restrict_boss_items": "none",
# Shuffle Ganon defaults to TRUE # Shuffle Ganon defaults to TRUE
@@ -157,7 +164,6 @@ def parse_settings():
"overworld_map": "default", "overworld_map": "default",
"pseudoboots": False, "pseudoboots": False,
"shufflepots": False,
"shuffleenemies": "none", "shuffleenemies": "none",
"shufflebosses": "none", "shufflebosses": "none",
"enemy_damage": "default", "enemy_damage": "default",
@@ -165,7 +171,11 @@ def parse_settings():
"enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"), "enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"),
"shopsanity": False, "shopsanity": False,
"keydropshuffle": False, 'keydropshuffle': False,
'dropshuffle': False,
'pottery': 'none',
'colorizepots': False,
'shufflepots': False,
"mapshuffle": False, "mapshuffle": False,
"compassshuffle": False, "compassshuffle": False,
"keyshuffle": False, "keyshuffle": False,
@@ -202,6 +212,8 @@ def parse_settings():
"uw_palettes": "default", "uw_palettes": "default",
"reduce_flashing": False, "reduce_flashing": False,
"shuffle_sfx": False, "shuffle_sfx": False,
'msu_resume': False,
'collection_rate': False,
# Spoiler defaults to TRUE # Spoiler defaults to TRUE
# Playthrough defaults to TRUE # Playthrough defaults to TRUE
@@ -209,9 +221,11 @@ def parse_settings():
"create_spoiler": True, "create_spoiler": True,
"calc_playthrough": True, "calc_playthrough": True,
"create_rom": True, "create_rom": True,
"bps": False,
"usestartinventory": False, "usestartinventory": False,
"custom": False, "custom": False,
"rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"), "rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"),
"patch": os.path.join(".", "Patch File.bps"),
"seed": "", "seed": "",
"count": 1, "count": 1,
+78 -23
View File
@@ -6,6 +6,7 @@ from enum import unique, Flag
from typing import DefaultDict, Dict, List from typing import DefaultDict, Dict, List
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import PotFlags, LocationType
from Doors import reset_portals from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
from Dungeons import dungeon_bigs, dungeon_hints from Dungeons import dungeon_bigs, dungeon_hints
@@ -380,7 +381,7 @@ def choose_portals(world, player):
if world.doorShuffle[player] in ['basic', 'crossed']: if world.doorShuffle[player] in ['basic', 'crossed']:
cross_flag = world.doorShuffle[player] == 'crossed' cross_flag = world.doorShuffle[player] == 'crossed'
# key drops allow the big key in the right place in Desert Tiles 2 # key drops allow the big key in the right place in Desert Tiles 2
bk_shuffle = world.bigkeyshuffle[player] or world.keydropshuffle[player] bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player]
std_flag = world.mode[player] == 'standard' std_flag = world.mode[player] == 'standard'
# roast incognito doors # roast incognito doors
world.get_room(0x60, player).delete(5) world.get_room(0x60, player).delete(5)
@@ -414,7 +415,7 @@ def choose_portals(world, player):
info.sole_entrance = inaccessible_portals[0] info.sole_entrance = inaccessible_portals[0]
info.required_passage.clear() info.required_passage.clear()
else: else:
raise Exception('please inspect this case') raise Exception(f'No reachable entrances for {dungeon}')
if len(reachable_portals) == 1: if len(reachable_portals) == 1:
info.sole_entrance = reachable_portals[0] info.sole_entrance = reachable_portals[0]
info_map[dungeon] = info info_map[dungeon] = info
@@ -521,7 +522,7 @@ def analyze_portals(world, player):
info.sole_entrance = inaccessible_portals[0] info.sole_entrance = inaccessible_portals[0]
info.required_passage.clear() info.required_passage.clear()
else: else:
raise Exception('please inspect this case') raise Exception(f'No reachable entrances for {dungeon}')
if len(reachable_portals) == 1: if len(reachable_portals) == 1:
info.sole_entrance = reachable_portals[0] info.sole_entrance = reachable_portals[0]
if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace': if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace':
@@ -996,10 +997,15 @@ def cross_dungeon(world, player):
assign_cross_keys(dungeon_builders, world, player) assign_cross_keys(dungeon_builders, world, player)
all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items))
if world.keydropshuffle[player]: target_items = 34
target_items = 35 if world.retro[player] else 96 if world.retro[player]:
target_items += 1 if world.dropshuffle[player] else 0 # the hc big key
else: else:
target_items = 34 if world.retro[player] else 63 target_items += 29 # small keys in chests
if world.dropshuffle[player]:
target_items += 14 # 13 dropped smalls + 1 big
if world.pottery[player] not in ['none', 'cave']:
target_items += 19 # 19 pot keys
d_items = target_items - all_dungeon_items_cnt d_items = target_items - all_dungeon_items_cnt
world.pool_adjustment[player] = d_items world.pool_adjustment[player] = d_items
smooth_door_pairs(world, player) smooth_door_pairs(world, player)
@@ -1057,7 +1063,11 @@ def assign_cross_keys(dungeon_builders, world, player):
logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors")) logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors"))
start = time.process_time() start = time.process_time()
if world.retro[player]: if world.retro[player]:
remaining = 61 if world.keydropshuffle[player] else 29 remaining = 29
if world.dropshuffle[player]:
remaining += 13
if world.pottery[player] not in ['none', 'cave']:
remaining += 19
else: else:
remaining = len(list(x for dgn in world.dungeons if dgn.player == player for x in dgn.small_keys)) remaining = len(list(x for dgn in world.dungeons if dgn.player == player for x in dgn.small_keys))
total_keys = remaining total_keys = remaining
@@ -1076,11 +1086,10 @@ def assign_cross_keys(dungeon_builders, world, player):
total_candidates += builder.key_doors_num total_candidates += builder.key_doors_num
start_regions_map[name] = start_regions start_regions_map[name] = start_regions
# Step 2: Initial Key Number Assignment & Calculate Flexibility # Step 2: Initial Key Number Assignment & Calculate Flexibility
for name, builder in dungeon_builders.items(): for name, builder in dungeon_builders.items():
calculated = int(round(builder.key_doors_num*total_keys/total_candidates)) calculated = int(round(builder.key_doors_num*total_keys/total_candidates))
max_keys = builder.location_cnt - calc_used_dungeon_items(builder) max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder))
cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt) cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt)
limit = min(max_keys, cand_len) limit = min(max_keys, cand_len)
suggested = min(calculated, limit) suggested = min(calculated, limit)
@@ -1253,7 +1262,13 @@ def refine_hints(dungeon_builders):
for region in builder.master_sector.regions: for region in builder.master_sector.regions:
for location in region.locations: for location in region.locations:
if not location.event and '- Boss' not in location.name and '- Prize' not in location.name and location.name != 'Sanctuary': if not location.event and '- Boss' not in location.name and '- Prize' not in location.name and location.name != 'Sanctuary':
location.hint_text = dungeon_hints[name] if location.type == LocationType.Pot and location.pot:
hint_text = ('under a block' if location.pot.flags & PotFlags.Block else 'in a pot')
location.hint_text = f'{hint_text} {dungeon_hints[name]}'
elif location.type == LocationType.Drop:
location.hint_text = f'dropped {dungeon_hints[name]}'
else:
location.hint_text = dungeon_hints[name]
def refine_boss_exits(world, player): def refine_boss_exits(world, player):
@@ -1729,7 +1744,7 @@ def smooth_door_pairs(world, player):
if type_b == DoorKind.SmallKey: if type_b == DoorKind.SmallKey:
remove_pair(door, world, player) remove_pair(door, world, player)
else: else:
if valid_pair: if valid_pair and not std_forbidden(door, world, player):
bd_candidates[door.entrance.parent_region.dungeon].append(door) bd_candidates[door.entrance.parent_region.dungeon].append(door)
elif type_a in [DoorKind.Bombable, DoorKind.Dashable] or type_b in [DoorKind.Bombable, DoorKind.Dashable]: elif type_a in [DoorKind.Bombable, DoorKind.Dashable] or type_b in [DoorKind.Bombable, DoorKind.Dashable]:
if type_a in [DoorKind.Bombable, DoorKind.Dashable]: if type_a in [DoorKind.Bombable, DoorKind.Dashable]:
@@ -1738,7 +1753,8 @@ def smooth_door_pairs(world, player):
else: else:
room_b.change(partner.doorListPos, DoorKind.Normal) room_b.change(partner.doorListPos, DoorKind.Normal)
remove_pair(partner, world, player) remove_pair(partner, world, player)
elif valid_pair and type_a != DoorKind.SmallKey and type_b != DoorKind.SmallKey: elif (valid_pair and type_a != DoorKind.SmallKey and type_b != DoorKind.SmallKey
and not std_forbidden(door, world, player)):
bd_candidates[door.entrance.parent_region.dungeon].append(door) bd_candidates[door.entrance.parent_region.dungeon].append(door)
shuffle_bombable_dashable(bd_candidates, world, player) shuffle_bombable_dashable(bd_candidates, world, player)
world.paired_doors[player] = [x for x in world.paired_doors[player] if x.pair or x.original] world.paired_doors[player] = [x for x in world.paired_doors[player] if x.pair or x.original]
@@ -1777,12 +1793,37 @@ def stateful_door(door, kind):
return False return False
def std_forbidden(door, world, player):
return (world.mode[player] == 'standard' and door.entrance.parent_region.dungeon.name == 'Hyrule Castle' and
'Hyrule Castle Throne Room N' in [door.name, door.dest.name])
dashable_forbidden = {
'Swamp Trench 1 Key Ledge NW', 'Swamp Left Elbow WN', 'Swamp Right Elbow SE', 'Mire Hub WN', 'Mire Hub WS',
'Mire Hub Top NW', 'Mire Hub NE', 'Ice Dead End WS'
}
ohko_forbidden = {
'GT Invisible Catwalk NE', 'GT Falling Bridge WN', 'GT Falling Bridge WS', 'GT Hidden Star ES', 'GT Hookshot EN',
'GT Torch Cross WN', 'TR Torches WN', 'Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Ice Hookshot Balcony SW',
'Ice Catwalk WN', 'Ice Catwalk NW', 'Ice Bomb Jump NW', 'GT Cannonball Bridge SE'
}
def filter_dashable_candidates(candidates, world):
forbidden_set = dashable_forbidden
if world.timer in ['ohko', 'timed-ohko']:
forbidden_set = ohko_forbidden.union(dashable_forbidden)
return [x for x in candidates if x.name not in forbidden_set and x.dest.name not in forbidden_set]
def shuffle_bombable_dashable(bd_candidates, world, player): def shuffle_bombable_dashable(bd_candidates, world, player):
if world.doorShuffle[player] == 'basic': if world.doorShuffle[player] == 'basic':
for dungeon, candidates in bd_candidates.items(): for dungeon, candidates in bd_candidates.items():
diff = bomb_dash_counts[dungeon.name][1] diff = bomb_dash_counts[dungeon.name][1]
if diff > 0: if diff > 0:
for chosen in random.sample(candidates, min(diff, len(candidates))): dash_candidates = filter_dashable_candidates(candidates, world)
for chosen in random.sample(dash_candidates, min(diff, len(candidates))):
change_pair_type(chosen, DoorKind.Dashable, world, player) change_pair_type(chosen, DoorKind.Dashable, world, player)
candidates.remove(chosen) candidates.remove(chosen)
diff = bomb_dash_counts[dungeon.name][0] diff = bomb_dash_counts[dungeon.name][0]
@@ -1794,7 +1835,8 @@ def shuffle_bombable_dashable(bd_candidates, world, player):
remove_pair_type_if_present(excluded, world, player) remove_pair_type_if_present(excluded, world, player)
elif world.doorShuffle[player] == 'crossed': elif world.doorShuffle[player] == 'crossed':
all_candidates = sum(bd_candidates.values(), []) all_candidates = sum(bd_candidates.values(), [])
for chosen in random.sample(all_candidates, min(8, len(all_candidates))): dash_candidates = filter_dashable_candidates(all_candidates, world)
for chosen in random.sample(dash_candidates, min(8, len(all_candidates))):
change_pair_type(chosen, DoorKind.Dashable, world, player) change_pair_type(chosen, DoorKind.Dashable, world, player)
all_candidates.remove(chosen) all_candidates.remove(chosen)
for chosen in random.sample(all_candidates, min(12, len(all_candidates))): for chosen in random.sample(all_candidates, min(12, len(all_candidates))):
@@ -2113,6 +2155,7 @@ logical_connections = [
('Hera Startile Wide Crystal Exit', 'Hera Startile Wide'), ('Hera Startile Wide Crystal Exit', 'Hera Startile Wide'),
('Hera Big Chest Hook Path', 'Hera Big Chest Landing'), ('Hera Big Chest Hook Path', 'Hera Big Chest Landing'),
('Hera Big Chest Landing Exit', 'Hera 4F'), ('Hera Big Chest Landing Exit', 'Hera 4F'),
('Hera 5F Orange Path', 'Hera 5F Pot Block'),
('PoD Pit Room Block Path N', 'PoD Pit Room Blocked'), ('PoD Pit Room Block Path N', 'PoD Pit Room Blocked'),
('PoD Pit Room Block Path S', 'PoD Pit Room'), ('PoD Pit Room Block Path S', 'PoD Pit Room'),
@@ -2176,6 +2219,7 @@ logical_connections = [
('Swamp Trench 1 Departure Approach', 'Swamp Trench 1 Approach'), ('Swamp Trench 1 Departure Approach', 'Swamp Trench 1 Approach'),
('Swamp Trench 1 Departure Key', 'Swamp Trench 1 Key Ledge'), ('Swamp Trench 1 Departure Key', 'Swamp Trench 1 Key Ledge'),
('Swamp Hub Hook Path', 'Swamp Hub North Ledge'), ('Swamp Hub Hook Path', 'Swamp Hub North Ledge'),
('Swamp Hub Side Hook Path', 'Swamp Hub Side Ledges'),
('Swamp Hub North Ledge Drop Down', 'Swamp Hub'), ('Swamp Hub North Ledge Drop Down', 'Swamp Hub'),
('Swamp Crystal Switch Outer to Inner Barrier - Blue', 'Swamp Crystal Switch Inner'), ('Swamp Crystal Switch Outer to Inner Barrier - Blue', 'Swamp Crystal Switch Inner'),
('Swamp Crystal Switch Outer to Ranged Crystal', 'Swamp Crystal Switch Outer - Ranged Crystal'), ('Swamp Crystal Switch Outer to Ranged Crystal', 'Swamp Crystal Switch Outer - Ranged Crystal'),
@@ -2207,13 +2251,17 @@ logical_connections = [
('Skull Pot Circle Star Path', 'Skull Map Room'), ('Skull Pot Circle Star Path', 'Skull Map Room'),
('Skull Big Chest Hookpath', 'Skull 1 Lobby'), ('Skull Big Chest Hookpath', 'Skull 1 Lobby'),
('Skull Back Drop Star Path', 'Skull Small Hall'), ('Skull Back Drop Star Path', 'Skull Small Hall'),
('Skull 2 West Lobby Pits', 'Skull 2 West Lobby Ledge'),
('Skull 2 West Lobby Ledge Pits', 'Skull 2 West Lobby'),
('Thieves Rail Ledge Drop Down', 'Thieves BK Corner'), ('Thieves Rail Ledge Drop Down', 'Thieves BK Corner'),
('Thieves Hellway Orange Barrier', 'Thieves Hellway S Crystal'), ('Thieves Hellway Orange Barrier', 'Thieves Hellway S Crystal'),
('Thieves Hellway Crystal Orange Barrier', 'Thieves Hellway'), ('Thieves Hellway Crystal Orange Barrier', 'Thieves Hellway'),
('Thieves Hellway Blue Barrier', 'Thieves Hellway N Crystal'), ('Thieves Hellway Blue Barrier', 'Thieves Hellway N Crystal'),
('Thieves Hellway Crystal Blue Barrier', 'Thieves Hellway'), ('Thieves Hellway Crystal Blue Barrier', 'Thieves Hellway'),
('Thieves Attic Orange Barrier', 'Thieves Attic Hint'), ('Thieves Attic Orange Barrier', 'Thieves Attic Hint'),
('Thieves Attic Blue Barrier', 'Thieves Attic Switch'),
('Thieves Attic Hint Orange Barrier', 'Thieves Attic'), ('Thieves Attic Hint Orange Barrier', 'Thieves Attic'),
('Thieves Attic Switch Blue Barrier', 'Thieves Attic'),
('Thieves Basement Block Path', 'Thieves Blocked Entry'), ('Thieves Basement Block Path', 'Thieves Blocked Entry'),
('Thieves Blocked Entry Path', 'Thieves Basement Block'), ('Thieves Blocked Entry Path', 'Thieves Basement Block'),
('Thieves Conveyor Bridge Block Path', 'Thieves Conveyor Block'), ('Thieves Conveyor Bridge Block Path', 'Thieves Conveyor Block'),
@@ -2272,6 +2320,8 @@ logical_connections = [
('TR Main Lobby Gap', 'TR Lobby Ledge'), ('TR Main Lobby Gap', 'TR Lobby Ledge'),
('TR Lobby Ledge Gap', 'TR Main Lobby'), ('TR Lobby Ledge Gap', 'TR Main Lobby'),
('TR Hub Path', 'TR Hub Ledges'),
('TR Hub Ledges Path', 'TR Hub'),
('TR Pipe Ledge Drop Down', 'TR Pipe Pit'), ('TR Pipe Ledge Drop Down', 'TR Pipe Pit'),
('TR Big Chest Gap', 'TR Big Chest Entrance'), ('TR Big Chest Gap', 'TR Big Chest Entrance'),
('TR Big Chest Entrance Gap', 'TR Big Chest'), ('TR Big Chest Entrance Gap', 'TR Big Chest'),
@@ -2300,6 +2350,8 @@ logical_connections = [
('TR Crystaroller Chest to Middle Barrier - Blue', 'TR Crystaroller Middle'), ('TR Crystaroller Chest to Middle Barrier - Blue', 'TR Crystaroller Middle'),
('TR Crystaroller Middle Ranged Crystal Exit', 'TR Crystaroller Middle'), ('TR Crystaroller Middle Ranged Crystal Exit', 'TR Crystaroller Middle'),
('TR Crystaroller Bottom Ranged Crystal Exit', 'TR Crystaroller Bottom'), ('TR Crystaroller Bottom Ranged Crystal Exit', 'TR Crystaroller Bottom'),
('TR Dark Ride Path', 'TR Dark Ride Ledges'),
('TR Dark Ride Ledges Path', 'TR Dark Ride'),
('TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze Interior'), ('TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze Interior'),
('TR Crystal Maze Start to Crystal', 'TR Crystal Maze Start - Crystal'), ('TR Crystal Maze Start to Crystal', 'TR Crystal Maze Start - Crystal'),
('TR Crystal Maze Start Crystal Exit', 'TR Crystal Maze Start'), ('TR Crystal Maze Start Crystal Exit', 'TR Crystal Maze Start'),
@@ -2310,16 +2362,18 @@ logical_connections = [
('TR Crystal Maze End to Interior Barrier - Blue', 'TR Crystal Maze Interior'), ('TR Crystal Maze End to Interior Barrier - Blue', 'TR Crystal Maze Interior'),
('TR Crystal Maze End to Ranged Crystal', 'TR Crystal Maze End - Ranged Crystal'), ('TR Crystal Maze End to Ranged Crystal', 'TR Crystal Maze End - Ranged Crystal'),
('TR Crystal Maze End Ranged Crystal Exit', 'TR Crystal Maze End'), ('TR Crystal Maze End Ranged Crystal Exit', 'TR Crystal Maze End'),
('TR Final Abyss Balcony Path', 'TR Final Abyss Ledge'),
('TR Final Abyss Ledge Path', 'TR Final Abyss Balcony'),
('GT Blocked Stairs Block Path', 'GT Big Chest'), ('GT Blocked Stairs Block Path', 'GT Big Chest'),
('GT Speed Torch South Path', 'GT Speed Torch'), ('GT Speed Torch South Path', 'GT Speed Torch'),
('GT Speed Torch North Path', 'GT Speed Torch Upper'), ('GT Speed Torch North Path', 'GT Speed Torch Upper'),
('GT Hookshot East-North Path', 'GT Hookshot North Platform'), ('GT Hookshot East-Mid Path', 'GT Hookshot Mid Platform'),
('GT Hookshot East-South Path', 'GT Hookshot South Platform'), ('GT Hookshot Mid-East Path', 'GT Hookshot East Platform'),
('GT Hookshot North-East Path', 'GT Hookshot East Platform'), ('GT Hookshot North-Mid Path', 'GT Hookshot Mid Platform'),
('GT Hookshot North-South Path', 'GT Hookshot South Platform'), ('GT Hookshot Mid-North Path', 'GT Hookshot North Platform'),
('GT Hookshot South-East Path', 'GT Hookshot East Platform'), ('GT Hookshot South-Mid Path', 'GT Hookshot Mid Platform'),
('GT Hookshot South-North Path', 'GT Hookshot North Platform'), ('GT Hookshot Mid-South Path', 'GT Hookshot South Platform'),
('GT Hookshot Platform Blue Barrier', 'GT Hookshot South Entry'), ('GT Hookshot Platform Blue Barrier', 'GT Hookshot South Entry'),
('GT Hookshot Platform Barrier Bypass', 'GT Hookshot South Entry'), ('GT Hookshot Platform Barrier Bypass', 'GT Hookshot South Entry'),
('GT Hookshot Entry Blue Barrier', 'GT Hookshot South Platform'), ('GT Hookshot Entry Blue Barrier', 'GT Hookshot South Platform'),
@@ -2350,8 +2404,8 @@ logical_connections = [
('GT Crystal Conveyor to Corner Barrier - Blue', 'GT Crystal Conveyor Corner'), ('GT Crystal Conveyor to Corner Barrier - Blue', 'GT Crystal Conveyor Corner'),
('GT Crystal Conveyor to Ranged Crystal', 'GT Crystal Conveyor - Ranged Crystal'), ('GT Crystal Conveyor to Ranged Crystal', 'GT Crystal Conveyor - Ranged Crystal'),
('GT Crystal Conveyor Corner to Left Bypass', 'GT Crystal Conveyor Left'), ('GT Crystal Conveyor Corner to Left Bypass', 'GT Crystal Conveyor Left'),
('GT Crystal Conveyor Corner to Barrier - Blue', 'GT Crystal Conveyor Left'), ('GT Crystal Conveyor Corner to Barrier - Blue', 'GT Crystal Conveyor'),
('GT Crystal Conveyor Corner to Barrier - Orange', 'GT Crystal Conveyor'), ('GT Crystal Conveyor Corner to Barrier - Orange', 'GT Crystal Conveyor Left'),
('GT Crystal Conveyor Corner to Ranged Crystal', 'GT Crystal Conveyor Corner - Ranged Crystal'), ('GT Crystal Conveyor Corner to Ranged Crystal', 'GT Crystal Conveyor Corner - Ranged Crystal'),
('GT Crystal Conveyor Left to Corner Barrier - Orange', 'GT Crystal Conveyor Corner'), ('GT Crystal Conveyor Left to Corner Barrier - Orange', 'GT Crystal Conveyor Corner'),
('GT Crystal Conveyor Ranged Crystal Exit', 'GT Crystal Conveyor'), ('GT Crystal Conveyor Ranged Crystal Exit', 'GT Crystal Conveyor'),
@@ -3017,7 +3071,8 @@ palette_map = {
'Tower of Hera': (0x6, None), 'Tower of Hera': (0x6, None),
'Thieves Town': (0x17, None), # the attic uses 0x23 'Thieves Town': (0x17, None), # the attic uses 0x23
'Turtle Rock': (0x18, 0x19, 'TR Boss SW'), 'Turtle Rock': (0x18, 0x19, 'TR Boss SW'),
'Ganons Tower': (0x28, 0x1b, 'GT Agahnim 2 SW'), # other palettes: 0x1a (other) 0x24 (Gauntlet - Lanmo) 0x25 (conveyor-torch-wizzrode moldorm pit f5?) 'Ganons Tower': (0x28, 0x1b, 'GT Agahnim 2 SW'),
# other palettes: 0x1a (other) 0x24 (Gauntlet - Lanmo) 0x25 (conveyor-torch-wizzrobe moldorm pit f5?)
} }
# implications: # implications:
+22 -8
View File
@@ -309,6 +309,7 @@ def create_doors(world, player):
create_door(player, 'Hera 5F Star Hole', Hole), create_door(player, 'Hera 5F Star Hole', Hole),
create_door(player, 'Hera 5F Pothole Chain', Hole), create_door(player, 'Hera 5F Pothole Chain', Hole),
create_door(player, 'Hera 5F Normal Holes', Hole), create_door(player, 'Hera 5F Normal Holes', Hole),
create_door(player, 'Hera 5F Orange Path', Lgcl),
create_door(player, 'Hera Fairies\' Warp', Warp), create_door(player, 'Hera Fairies\' Warp', Warp),
create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0).kill(), create_door(player, 'Hera Boss Down Stairs', Sprl).dir(Dn, 0x07, 0, HTH).ss(S, 0x61, 0xb0).kill(),
create_door(player, 'Hera Boss Outer Hole', Hole), create_door(player, 'Hera Boss Outer Hole', Hole),
@@ -500,6 +501,7 @@ def create_doors(world, player):
create_door(player, 'Swamp Hub WS', Nrml).dir(We, 0x36, Bot, High).pos(3), create_door(player, 'Swamp Hub WS', Nrml).dir(We, 0x36, Bot, High).pos(3),
create_door(player, 'Swamp Hub WN', Nrml).dir(We, 0x36, Top, High).small_key().pos(2), create_door(player, 'Swamp Hub WN', Nrml).dir(We, 0x36, Top, High).small_key().pos(2),
create_door(player, 'Swamp Hub Hook Path', Lgcl), create_door(player, 'Swamp Hub Hook Path', Lgcl),
create_door(player, 'Swamp Hub Side Hook Path', Lgcl),
create_door(player, 'Swamp Hub Dead Ledge EN', Nrml).dir(Ea, 0x36, Top, High).pos(0), create_door(player, 'Swamp Hub Dead Ledge EN', Nrml).dir(Ea, 0x36, Top, High).pos(0),
create_door(player, 'Swamp Hub North Ledge N', Nrml).dir(No, 0x36, Mid, High).small_key().pos(1), create_door(player, 'Swamp Hub North Ledge N', Nrml).dir(No, 0x36, Mid, High).small_key().pos(1),
create_door(player, 'Swamp Hub North Ledge Drop Down', Lgcl), create_door(player, 'Swamp Hub North Ledge Drop Down', Lgcl),
@@ -612,6 +614,8 @@ def create_doors(world, player):
create_door(player, 'Skull 2 West Lobby S', Nrml).dir(So, 0x56, Left, High).pos(1).portal(Z, 0x00), create_door(player, 'Skull 2 West Lobby S', Nrml).dir(So, 0x56, Left, High).pos(1).portal(Z, 0x00),
create_door(player, 'Skull 2 West Lobby ES', Intr).dir(Ea, 0x56, Bot, High).pos(2), create_door(player, 'Skull 2 West Lobby ES', Intr).dir(Ea, 0x56, Bot, High).pos(2),
create_door(player, 'Skull 2 West Lobby NW', Intr).dir(No, 0x56, Left, High).small_key().pos(0), create_door(player, 'Skull 2 West Lobby NW', Intr).dir(No, 0x56, Left, High).small_key().pos(0),
create_door(player, 'Skull 2 West Lobby Pits', Lgcl),
create_door(player, 'Skull 2 West Lobby Ledge Pits', Lgcl),
create_door(player, 'Skull X Room SW', Intr).dir(So, 0x56, Left, High).small_key().pos(0), create_door(player, 'Skull X Room SW', Intr).dir(So, 0x56, Left, High).small_key().pos(0),
create_door(player, 'Skull Back Drop Star Path', Lgcl), create_door(player, 'Skull Back Drop Star Path', Lgcl),
create_door(player, 'Skull 3 Lobby SW', Nrml).dir(So, 0x59, Left, High).pos(1).portal(Z, 0x02), create_door(player, 'Skull 3 Lobby SW', Nrml).dir(So, 0x59, Left, High).pos(1).portal(Z, 0x02),
@@ -684,7 +688,9 @@ def create_doors(world, player):
create_door(player, 'Thieves Attic Down Stairs', Sprl).dir(Dn, 0x64, 0, HTH).ss(Z, 0x11, 0x80, True, True), create_door(player, 'Thieves Attic Down Stairs', Sprl).dir(Dn, 0x64, 0, HTH).ss(Z, 0x11, 0x80, True, True),
create_door(player, 'Thieves Attic ES', Intr).dir(Ea, 0x64, Bot, High).pos(0), create_door(player, 'Thieves Attic ES', Intr).dir(Ea, 0x64, Bot, High).pos(0),
create_door(player, 'Thieves Attic Orange Barrier', Lgcl), create_door(player, 'Thieves Attic Orange Barrier', Lgcl),
create_door(player, 'Thieves Attic Blue Barrier', Lgcl),
create_door(player, 'Thieves Attic Hint Orange Barrier', Lgcl), create_door(player, 'Thieves Attic Hint Orange Barrier', Lgcl),
create_door(player, 'Thieves Attic Switch Blue Barrier', Lgcl),
create_door(player, 'Thieves Cricket Hall Left WS', Intr).dir(We, 0x64, Bot, High).pos(0), create_door(player, 'Thieves Cricket Hall Left WS', Intr).dir(We, 0x64, Bot, High).pos(0),
create_door(player, 'Thieves Cricket Hall Left Edge', Open).dir(Ea, 0x64, None, High).edge(0, X, 0x30), create_door(player, 'Thieves Cricket Hall Left Edge', Open).dir(Ea, 0x64, None, High).edge(0, X, 0x30),
create_door(player, 'Thieves Cricket Hall Right Edge', Open).dir(We, 0x65, None, High).edge(0, Z, 0x30), create_door(player, 'Thieves Cricket Hall Right Edge', Open).dir(We, 0x65, None, High).edge(0, Z, 0x30),
@@ -955,6 +961,8 @@ def create_doors(world, player):
create_door(player, 'TR Hub EN', Nrml).dir(Ea, 0xc6, Top, High).pos(2), create_door(player, 'TR Hub EN', Nrml).dir(Ea, 0xc6, Top, High).pos(2),
create_door(player, 'TR Hub NW', Nrml).dir(No, 0xc6, Left, High).small_key().pos(0), create_door(player, 'TR Hub NW', Nrml).dir(No, 0xc6, Left, High).small_key().pos(0),
create_door(player, 'TR Hub NE', Nrml).dir(No, 0xc6, Right, High).pos(1), create_door(player, 'TR Hub NE', Nrml).dir(No, 0xc6, Right, High).pos(1),
create_door(player, 'TR Hub Path', Lgcl),
create_door(player, 'TR Hub Ledges Path', Lgcl),
create_door(player, 'TR Torches Ledge WS', Nrml).dir(We, 0xc7, Bot, High).pos(2), create_door(player, 'TR Torches Ledge WS', Nrml).dir(We, 0xc7, Bot, High).pos(2),
create_door(player, 'TR Torches WN', Nrml).dir(We, 0xc7, Top, High).pos(1), create_door(player, 'TR Torches WN', Nrml).dir(We, 0xc7, Top, High).pos(1),
create_door(player, 'TR Torches NW', Nrml).dir(No, 0xc7, Left, High).trap(0x4).pos(0), create_door(player, 'TR Torches NW', Nrml).dir(No, 0xc7, Left, High).trap(0x4).pos(0),
@@ -1030,6 +1038,8 @@ def create_doors(world, player):
create_door(player, 'TR Crystaroller Down Stairs', Sprl).dir(Dn, 0x04, 0, HTH).ss(A, 0x12, 0x80, True, True).small_key().pos(0), create_door(player, 'TR Crystaroller Down Stairs', Sprl).dir(Dn, 0x04, 0, HTH).ss(A, 0x12, 0x80, True, True).small_key().pos(0),
create_door(player, 'TR Dark Ride Up Stairs', Sprl).dir(Up, 0xb5, 0, HTH).ss(A, 0x1b, 0x6c), create_door(player, 'TR Dark Ride Up Stairs', Sprl).dir(Up, 0xb5, 0, HTH).ss(A, 0x1b, 0x6c),
create_door(player, 'TR Dark Ride SW', Nrml).dir(So, 0xb5, Left, High).trap(0x4).pos(0).portal(Z, 0x22), create_door(player, 'TR Dark Ride SW', Nrml).dir(So, 0xb5, Left, High).trap(0x4).pos(0).portal(Z, 0x22),
create_door(player, 'TR Dark Ride Path', Lgcl),
create_door(player, 'TR Dark Ride Ledges Path', Lgcl),
create_door(player, 'TR Dash Bridge NW', Nrml).dir(No, 0xc5, Left, High).pos(1), create_door(player, 'TR Dash Bridge NW', Nrml).dir(No, 0xc5, Left, High).pos(1),
create_door(player, 'TR Dash Bridge SW', Nrml).dir(So, 0xc5, Left, High).pos(2).portal(Z, 0x02), create_door(player, 'TR Dash Bridge SW', Nrml).dir(So, 0xc5, Left, High).pos(2).portal(Z, 0x02),
create_door(player, 'TR Dash Bridge WS', Nrml).dir(We, 0xc5, Bot, High).small_key().pos(0), create_door(player, 'TR Dash Bridge WS', Nrml).dir(We, 0xc5, Bot, High).small_key().pos(0),
@@ -1048,6 +1058,8 @@ def create_doors(world, player):
create_door(player, 'TR Crystal Maze End Ranged Crystal Exit', Lgcl), create_door(player, 'TR Crystal Maze End Ranged Crystal Exit', Lgcl),
create_door(player, 'TR Crystal Maze North Stairs', StrS).dir(No, 0xc4, Mid, High), create_door(player, 'TR Crystal Maze North Stairs', StrS).dir(No, 0xc4, Mid, High),
create_door(player, 'TR Final Abyss South Stairs', StrS).dir(So, 0xb4, Mid, High), create_door(player, 'TR Final Abyss South Stairs', StrS).dir(So, 0xb4, Mid, High),
create_door(player, 'TR Final Abyss Balcony Path', Lgcl),
create_door(player, 'TR Final Abyss Ledge Path', Lgcl),
create_door(player, 'TR Final Abyss NW', Nrml).dir(No, 0xb4, Left, High).big_key().pos(0), create_door(player, 'TR Final Abyss NW', Nrml).dir(No, 0xb4, Left, High).big_key().pos(0),
create_door(player, 'TR Boss SW', Nrml).dir(So, 0xa4, Left, High).no_exit().trap(0x4).pos(0), # .portal(Z, 0x00), -enemizer doesn't work create_door(player, 'TR Boss SW', Nrml).dir(So, 0xa4, Left, High).no_exit().trap(0x4).pos(0), # .portal(Z, 0x00), -enemizer doesn't work
@@ -1098,12 +1110,12 @@ def create_doors(world, player):
create_door(player, 'GT Conveyor Cross EN', Nrml).dir(Ea, 0x8b, Top, High).pos(2), create_door(player, 'GT Conveyor Cross EN', Nrml).dir(Ea, 0x8b, Top, High).pos(2),
create_door(player, 'GT Conveyor Cross WN', Intr).dir(We, 0x8b, Top, High).pos(0), create_door(player, 'GT Conveyor Cross WN', Intr).dir(We, 0x8b, Top, High).pos(0),
create_door(player, 'GT Hookshot EN', Intr).dir(Ea, 0x8b, Top, High).pos(0), create_door(player, 'GT Hookshot EN', Intr).dir(Ea, 0x8b, Top, High).pos(0),
create_door(player, 'GT Hookshot East-North Path', Lgcl), create_door(player, 'GT Hookshot East-Mid Path', Lgcl),
create_door(player, 'GT Hookshot East-South Path', Lgcl), create_door(player, 'GT Hookshot Mid-East Path', Lgcl),
create_door(player, 'GT Hookshot North-East Path', Lgcl), create_door(player, 'GT Hookshot North-Mid Path', Lgcl),
create_door(player, 'GT Hookshot North-South Path', Lgcl), create_door(player, 'GT Hookshot Mid-North Path', Lgcl),
create_door(player, 'GT Hookshot South-East Path', Lgcl), create_door(player, 'GT Hookshot South-Mid Path', Lgcl),
create_door(player, 'GT Hookshot South-North Path', Lgcl), create_door(player, 'GT Hookshot Mid-South Path', Lgcl),
create_door(player, 'GT Hookshot Platform Blue Barrier', Lgcl), create_door(player, 'GT Hookshot Platform Blue Barrier', Lgcl),
create_door(player, 'GT Hookshot Entry Blue Barrier', Lgcl), create_door(player, 'GT Hookshot Entry Blue Barrier', Lgcl),
create_door(player, 'GT Hookshot Platform Barrier Bypass', Lgcl), create_door(player, 'GT Hookshot Platform Barrier Bypass', Lgcl),
@@ -1291,6 +1303,7 @@ def create_doors(world, player):
world.get_door('Hera Beetles Holes Front', player).c_switch() world.get_door('Hera Beetles Holes Front', player).c_switch()
world.get_door('Hera Beetles Holes Landing', player).c_switch() world.get_door('Hera Beetles Holes Landing', player).c_switch()
world.get_door('Hera Startile Wide Crystal Exit', player).c_switch() world.get_door('Hera Startile Wide Crystal Exit', player).c_switch()
world.get_door('Hera 5F Orange Path', player).barrier(CrystalBarrier.Orange)
world.get_door('PoD Arena North to Landing Barrier - Orange', player).barrier(CrystalBarrier.Orange) world.get_door('PoD Arena North to Landing Barrier - Orange', player).barrier(CrystalBarrier.Orange)
world.get_door('PoD Arena Main to Landing Barrier - Blue', player).barrier(CrystalBarrier.Blue) world.get_door('PoD Arena Main to Landing Barrier - Blue', player).barrier(CrystalBarrier.Blue)
@@ -1345,7 +1358,9 @@ def create_doors(world, player):
world.get_door('Thieves Hellway Orange Barrier', player).barrier(CrystalBarrier.Orange) world.get_door('Thieves Hellway Orange Barrier', player).barrier(CrystalBarrier.Orange)
world.get_door('Thieves Hellway Crystal Orange Barrier', player).barrier(CrystalBarrier.Orange) world.get_door('Thieves Hellway Crystal Orange Barrier', player).barrier(CrystalBarrier.Orange)
world.get_door('Thieves Attic Orange Barrier', player).barrier(CrystalBarrier.Orange) world.get_door('Thieves Attic Orange Barrier', player).barrier(CrystalBarrier.Orange)
world.get_door('Thieves Attic Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Thieves Attic Hint Orange Barrier', player).barrier(CrystalBarrier.Orange) world.get_door('Thieves Attic Hint Orange Barrier', player).barrier(CrystalBarrier.Orange)
world.get_door('Thieves Attic Switch Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Ice Bomb Drop SE', player).c_switch() world.get_door('Ice Bomb Drop SE', player).c_switch()
world.get_door('Ice Conveyor Crystal Exit', player).c_switch() world.get_door('Ice Conveyor Crystal Exit', player).c_switch()
@@ -1421,8 +1436,7 @@ def create_doors(world, player):
world.get_door('GT Crystal Conveyor Ranged Crystal Exit', player).c_switch() world.get_door('GT Crystal Conveyor Ranged Crystal Exit', player).c_switch()
world.get_door('GT Crystal Conveyor Corner Ranged Crystal Exit', player).c_switch() world.get_door('GT Crystal Conveyor Corner Ranged Crystal Exit', player).c_switch()
world.get_door('GT Crystal Conveyor Corner to Left Bypass', player).barrier(CrystalBarrier.Blue) world.get_door('GT Crystal Conveyor Corner to Left Bypass', player).barrier(CrystalBarrier.Blue)
world.get_door('GT Hookshot South-North Path', player).c_switch() world.get_door('GT Hookshot South-Mid Path', player).c_switch()
world.get_door('GT Hookshot South-East Path', player).c_switch()
world.get_door('GT Hookshot ES', player).c_switch() world.get_door('GT Hookshot ES', player).c_switch()
world.get_door('GT Hookshot Platform Barrier Bypass', player).barrier(CrystalBarrier.Orange) world.get_door('GT Hookshot Platform Barrier Bypass', player).barrier(CrystalBarrier.Orange)
world.get_door('GT Hookshot Platform Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('GT Hookshot Platform Blue Barrier', player).barrier(CrystalBarrier.Blue)
+81 -19
View File
@@ -1326,7 +1326,17 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
polarized_sectors[sector] = None polarized_sectors[sector] = None
if bow_sectors: if bow_sectors:
assign_bow_sectors(dungeon_map, bow_sectors, global_pole) assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player) leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player)
free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole)
for sector in free_location_sectors:
if sector.c_switch:
crystal_switches[sector] = None
elif sector.blue_barrier:
crystal_barriers[sector] = None
elif sector.polarity().is_neutral():
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, 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) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
for sector in leftover: for sector in leftover:
@@ -1558,33 +1568,85 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
assign_sector(sector_list[i], builder, bow_sectors, global_pole) assign_sector(sector_list[i], builder, bow_sectors, global_pole)
def assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player): def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole):
population = [n for n in dungeon_map.keys()]
k = round(len(free_location_sectors) * .50)
valid = False valid = False
choices = None choices = None
candidates = []
sector_list = list(free_location_sectors)
while not valid:
candidates = random.sample(sector_list, k=k)
choices = random.choices(population, k=len(candidates))
sector_dict = defaultdict(list)
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
sector_dict[builder].append(candidates[i])
valid = global_pole.is_valid_multi_choice_2(dungeon_map, dungeon_map.values(), sector_dict)
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
assign_sector(candidates[i], builder, free_location_sectors, global_pole)
return free_location_sectors
def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player):
valid = False
choices = defaultdict(list)
sector_list = list(free_location_sectors) sector_list = list(free_location_sectors)
random.shuffle(sector_list) random.shuffle(sector_list)
orig_location_set = build_orig_location_set(dungeon_map) orig_location_set = build_orig_location_set(dungeon_map)
num_dungeon_items = requested_dungeon_items(world, player) num_dungeon_items = requested_dungeon_items(world, player)
d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())}
next_sector = sector_list.pop()
while not valid: while not valid:
choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set, world, player)
location_set = {x: set(y) for x, y in orig_location_set.items()} if not choice:
for i, sector in enumerate(sector_list): break
d_name = choices[i].name choices[choice].append(next_sector)
choice = d_idx[d_name] if global_pole.is_valid_multi_choice_2(dungeon_map, dungeon_map.values(), choices):
totals[choice] += sector.chest_locations idx = d_idx[choice.name]
location_set[d_name].update(sector.chest_location_set) totals[idx] += next_sector.chest_locations
valid = True location_set[choice.name].update(next_sector.chest_location_set)
for d_name, idx in d_idx.items(): valid = True
free_items = count_reserved_locations(world, player, location_set[d_name]) for d_name, idx in d_idx.items():
target = max(free_items, 2) + num_dungeon_items free_items = count_reserved_locations(world, player, location_set[d_name])
if totals[idx] < target: target = max(free_items, 2) + num_dungeon_items
valid = False if totals[idx] < target:
break valid = False
for i, choice in enumerate(choices): break
builder = dungeon_map[choice.name] if not valid:
assign_sector(sector_list[i], builder, free_location_sectors, global_pole) if len(sector_list) == 0:
choices = defaultdict(list)
sector_list = list(free_location_sectors)
else:
next_sector = sector_list.pop()
else:
choices[choice].remove(next_sector)
for builder, choice_list in choices.items():
for choice in choice_list:
assign_sector(choice, builder, free_location_sectors, global_pole)
return free_location_sectors
def weighted_random_location(dungeon_map, choices, orig_location_set, world, player):
population = []
totals = []
location_set = {x: set(y) for x, y in orig_location_set.items()}
num_dungeon_items = requested_dungeon_items(world, player)
for i, dungeon_builder in enumerate(dungeon_map.values()):
ttl = dungeon_builder.location_cnt + sum(sector.chest_locations for sector in choices[dungeon_builder])
totals.append(ttl)
builder_set = location_set[dungeon_builder.name]
builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder])))
free_items = count_reserved_locations(world, player, builder_set)
target = max(free_items, 2) + num_dungeon_items
if ttl < target:
population.append(dungeon_builder)
choice = random.choice(population) if len(population) > 0 else None
return choice, totals, location_set
# deprecated
def weighted_random_locations(dungeon_map, free_location_sectors): def weighted_random_locations(dungeon_map, free_location_sectors):
population = [] population = []
ttl_assigned = 0 ttl_assigned = 0
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse if __name__ == '__main__':
import copy from source.meta.check_requirements import check_requirements
check_requirements(console=True)
import os import os
import logging import logging
import RaceRandom as random import RaceRandom as random
import textwrap
import shlex
import sys import sys
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
+35 -33
View File
@@ -84,7 +84,7 @@ hera_regions = [
'Hera Back - Ranged Crystal', 'Hera Basement Cage', 'Hera Basement Cage - Crystal', 'Hera Tile Room', 'Hera Back - Ranged Crystal', 'Hera Basement Cage', 'Hera Basement Cage - Crystal', 'Hera Tile Room',
'Hera Tridorm', 'Hera Tridorm - Crystal', 'Hera Torches', 'Hera Beetles', 'Hera Startile Corner', 'Hera Tridorm', 'Hera Tridorm - Crystal', 'Hera Torches', 'Hera Beetles', 'Hera Startile Corner',
'Hera Startile Wide', 'Hera Startile Wide - Crystal', 'Hera 4F', 'Hera Big Chest Landing', 'Hera 5F', 'Hera Startile Wide', 'Hera Startile Wide - Crystal', 'Hera 4F', 'Hera Big Chest Landing', 'Hera 5F',
'Hera Fairies', 'Hera Boss', 'Hera Portal' 'Hera 5F Pot Block', 'Hera Fairies', 'Hera Boss', 'Hera Portal'
] ]
tower_regions = [ tower_regions = [
@@ -110,24 +110,24 @@ pod_regions = [
swamp_regions = [ swamp_regions = [
'Swamp Lobby', 'Swamp Entrance', 'Swamp Pot Row', 'Swamp Map Ledge', 'Swamp Trench 1 Approach', 'Swamp Lobby', 'Swamp Entrance', 'Swamp Pot Row', 'Swamp Map Ledge', 'Swamp Trench 1 Approach',
'Swamp Trench 1 Nexus', 'Swamp Trench 1 Alcove', 'Swamp Trench 1 Key Ledge', 'Swamp Trench 1 Departure', 'Swamp Trench 1 Nexus', 'Swamp Trench 1 Alcove', 'Swamp Trench 1 Key Ledge', 'Swamp Trench 1 Departure',
'Swamp Hammer Switch', 'Swamp Hub', 'Swamp Hub Dead Ledge', 'Swamp Hub North Ledge', 'Swamp Donut Top', 'Swamp Hammer Switch', 'Swamp Hub', 'Swamp Hub Side Ledges', 'Swamp Hub Dead Ledge', 'Swamp Hub North Ledge',
'Swamp Donut Bottom', 'Swamp Compass Donut', 'Swamp Crystal Switch Outer', 'Swamp Crystal Switch Outer - Ranged Crystal', 'Swamp Donut Top', 'Swamp Donut Bottom', 'Swamp Compass Donut', 'Swamp Crystal Switch Outer',
'Swamp Crystal Switch Inner', 'Swamp Crystal Switch Inner - Crystal', 'Swamp Shortcut', 'Swamp Trench 2 Pots', 'Swamp Crystal Switch Outer - Ranged Crystal', 'Swamp Crystal Switch Inner', 'Swamp Crystal Switch Inner - Crystal',
'Swamp Trench 2 Blocks', 'Swamp Trench 2 Alcove', 'Swamp Trench 2 Departure', 'Swamp Big Key Ledge', 'Swamp Shortcut', 'Swamp Trench 2 Pots', 'Swamp Trench 2 Blocks', 'Swamp Trench 2 Alcove',
'Swamp West Shallows', 'Swamp West Block Path', 'Swamp West Ledge', 'Swamp Barrier Ledge', 'Swamp Barrier', 'Swamp Trench 2 Departure', 'Swamp Big Key Ledge', 'Swamp West Shallows', 'Swamp West Block Path',
'Swamp Attic', 'Swamp Push Statue', 'Swamp Shooters', 'Swamp Left Elbow', 'Swamp Right Elbow', 'Swamp Drain Left', 'Swamp West Ledge', 'Swamp Barrier Ledge', 'Swamp Barrier', 'Swamp Attic', 'Swamp Push Statue', 'Swamp Shooters',
'Swamp Drain Right', 'Swamp Flooded Room', 'Swamp Flooded Spot', 'Swamp Basement Shallows', 'Swamp Waterfall Room', 'Swamp Left Elbow', 'Swamp Right Elbow', 'Swamp Drain Left', 'Swamp Drain Right', 'Swamp Flooded Room',
'Swamp Refill', 'Swamp Behind Waterfall', 'Swamp C', 'Swamp Waterway', 'Swamp I', 'Swamp T', 'Swamp Boss', 'Swamp Flooded Spot', 'Swamp Basement Shallows', 'Swamp Waterfall Room', 'Swamp Refill', 'Swamp Behind Waterfall',
'Swamp Portal' 'Swamp C', 'Swamp Waterway', 'Swamp I', 'Swamp T', 'Swamp Boss', 'Swamp Portal'
] ]
skull_regions = [ skull_regions = [
'Skull 1 Lobby', 'Skull Map Room', 'Skull Pot Circle', 'Skull Pull Switch', 'Skull Big Chest', 'Skull Pinball', 'Skull 1 Lobby', 'Skull Map Room', 'Skull Pot Circle', 'Skull Pull Switch', 'Skull Big Chest', 'Skull Pinball',
'Skull Pot Prison', 'Skull Compass Room', 'Skull Left Drop', 'Skull 2 East Lobby', 'Skull Big Key', 'Skull Pot Prison', 'Skull Compass Room', 'Skull Left Drop', 'Skull 2 East Lobby', 'Skull Big Key',
'Skull Lone Pot', 'Skull Small Hall', 'Skull Back Drop', 'Skull 2 West Lobby', 'Skull X Room', 'Skull 3 Lobby', 'Skull Lone Pot', 'Skull Small Hall', 'Skull Back Drop', 'Skull 2 West Lobby', 'Skull 2 West Lobby Ledge',
'Skull East Bridge', 'Skull West Bridge Nook', 'Skull Star Pits', 'Skull Torch Room', 'Skull Vines', 'Skull X Room', 'Skull 3 Lobby', 'Skull East Bridge', 'Skull West Bridge Nook', 'Skull Star Pits',
'Skull Spike Corner', 'Skull Final Drop', 'Skull Boss', 'Skull 1 Portal', 'Skull 2 East Portal', 'Skull Torch Room', 'Skull Vines', 'Skull Spike Corner', 'Skull Final Drop', 'Skull Boss',
'Skull 2 West Portal', 'Skull 3 Portal' 'Skull 1 Portal', 'Skull 2 East Portal', 'Skull 2 West Portal', 'Skull 3 Portal'
] ]
thieves_regions = [ thieves_regions = [
@@ -135,10 +135,10 @@ thieves_regions = [
'Thieves Big Chest Nook', 'Thieves Hallway', 'Thieves Boss', 'Thieves Pot Alcove Mid', 'Thieves Pot Alcove Bottom', 'Thieves Big Chest Nook', 'Thieves Hallway', 'Thieves Boss', 'Thieves Pot Alcove Mid', 'Thieves Pot Alcove Bottom',
'Thieves Pot Alcove Top', 'Thieves Conveyor Maze', 'Thieves Spike Track', 'Thieves Hellway', 'Thieves Pot Alcove Top', 'Thieves Conveyor Maze', 'Thieves Spike Track', 'Thieves Hellway',
'Thieves Hellway N Crystal', 'Thieves Hellway S Crystal', 'Thieves Triple Bypass', 'Thieves Spike Switch', 'Thieves Hellway N Crystal', 'Thieves Hellway S Crystal', 'Thieves Triple Bypass', 'Thieves Spike Switch',
'Thieves Attic', 'Thieves Attic Hint', 'Thieves Cricket Hall Left', 'Thieves Cricket Hall Right', 'Thieves Attic', 'Thieves Attic Hint', 'Thieves Attic Switch', 'Thieves Cricket Hall Left',
'Thieves Attic Window', 'Thieves Basement Block', 'Thieves Blocked Entry', 'Thieves Lonely Zazak', 'Thieves Cricket Hall Right', 'Thieves Attic Window', 'Thieves Basement Block', 'Thieves Blocked Entry',
"Thieves Blind's Cell", "Thieves Blind's Cell Interior", 'Thieves Conveyor Bridge', 'Thieves Conveyor Block', 'Thieves Lonely Zazak', "Thieves Blind's Cell", "Thieves Blind's Cell Interior", 'Thieves Conveyor Bridge',
'Thieves Big Chest Room', 'Thieves Trap', 'Thieves Town Portal' 'Thieves Conveyor Block', 'Thieves Big Chest Room', 'Thieves Trap', 'Thieves Town Portal'
] ]
ice_regions = [ ice_regions = [
@@ -168,29 +168,31 @@ mire_regions = [
] ]
tr_regions = [ tr_regions = [
'TR Main Lobby', 'TR Lobby Ledge', 'TR Compass Room', 'TR Hub', 'TR Torches Ledge', 'TR Torches', 'TR Roller Room', 'TR Main Lobby', 'TR Lobby Ledge', 'TR Compass Room', 'TR Hub', 'TR Hub Ledges', 'TR Torches Ledge', 'TR Torches',
'TR Tile Room', 'TR Refill', 'TR Pokey 1', 'TR Chain Chomps Top', 'TR Chain Chomps Top - Crystal', 'TR Roller Room', 'TR Tile Room', 'TR Refill', 'TR Pokey 1', 'TR Chain Chomps Top', 'TR Chain Chomps Top - Crystal',
'TR Chain Chomps Bottom', 'TR Chain Chomps Bottom - Ranged Crystal', 'TR Pipe Pit', 'TR Pipe Ledge', 'TR Lava Dual Pipes', 'TR Chain Chomps Bottom', 'TR Chain Chomps Bottom - Ranged Crystal', 'TR Pipe Pit', 'TR Pipe Ledge',
'TR Lava Island', 'TR Lava Escape', 'TR Pokey 2 Top', 'TR Pokey 2 Top - Crystal', 'TR Pokey 2 Bottom', 'TR Pokey 2 Bottom - Ranged Crystal', 'TR Lava Dual Pipes', 'TR Lava Island', 'TR Lava Escape', 'TR Pokey 2 Top', 'TR Pokey 2 Top - Crystal',
'TR Twin Pokeys', 'TR Hallway', 'TR Dodgers', 'TR Big View','TR Big Chest', 'TR Big Chest Entrance', 'TR Pokey 2 Bottom', 'TR Pokey 2 Bottom - Ranged Crystal', 'TR Twin Pokeys', 'TR Hallway', 'TR Dodgers',
'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull', 'TR Rupees', 'TR Crystaroller Bottom', 'TR Big View', 'TR Big Chest', 'TR Big Chest Entrance', 'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull',
'TR Crystaroller Middle', 'TR Crystaroller Top', 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', 'TR Rupees', 'TR Crystaroller Bottom', 'TR Crystaroller Middle', 'TR Crystaroller Top',
'TR Crystaroller Middle - Ranged Crystal', 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', 'TR Crystaroller Middle - Ranged Crystal',
'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dark Ride Ledges', 'TR Dash Bridge', 'TR Eye Bridge',
'TR Crystal Maze Start', 'TR Crystal Maze Start - Crystal', 'TR Crystal Maze Interior', 'TR Crystal Maze End', 'TR Crystal Maze Start', 'TR Crystal Maze Start - Crystal', 'TR Crystal Maze Interior', 'TR Crystal Maze End',
'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss', 'TR Boss', 'Turtle Rock Main Portal', 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss Balcony', 'TR Final Abyss Ledge', 'TR Boss',
'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal', 'Turtle Rock Eye Bridge Portal' 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal',
'Turtle Rock Eye Bridge Portal'
] ]
gt_regions = [ gt_regions = [
'GT Lobby', 'GT Bob\'s Torch', 'GT Hope Room', 'GT Big Chest', 'GT Blocked Stairs', 'GT Bob\'s Room', 'GT Lobby', 'GT Bob\'s Torch', 'GT Hope Room', 'GT Big Chest', 'GT Blocked Stairs', 'GT Bob\'s Room',
'GT Tile Room', 'GT Speed Torch', 'GT Speed Torch Upper', 'GT Pots n Blocks', 'GT Crystal Conveyor', 'GT Tile Room', 'GT Speed Torch', 'GT Speed Torch Upper', 'GT Pots n Blocks', 'GT Crystal Conveyor',
'GT Crystal Conveyor Corner', 'GT Crystal Conveyor Left', 'GT Crystal Conveyor - Ranged Crystal', 'GT Crystal Conveyor Corner', 'GT Crystal Conveyor Left', 'GT Crystal Conveyor - Ranged Crystal',
'GT Crystal Conveyor Corner - Ranged Crystal', 'GT Crystal Conveyor Corner - Ranged Crystal', 'GT Compass Room', 'GT Invisible Bridges', 'GT Invisible Catwalk',
'GT Compass Room', 'GT Invisible Bridges', 'GT Invisible Catwalk', 'GT Conveyor Cross', 'GT Hookshot East Platform', 'GT Conveyor Cross', 'GT Hookshot East Platform', 'GT Hookshot Mid Platform', 'GT Hookshot North Platform',
'GT Hookshot North Platform', 'GT Hookshot South Platform', 'GT Hookshot South Entry', 'GT Hookshot South Entry - Ranged Crystal', 'GT Map Room', 'GT Hookshot South Platform', 'GT Hookshot South Entry', 'GT Hookshot South Entry - Ranged Crystal', 'GT Map Room',
'GT Double Switch Entry', 'GT Double Switch Pot Corners - Ranged Switches', 'GT Double Switch Pot Corners', 'GT Double Switch Entry', 'GT Double Switch Pot Corners - Ranged Switches', 'GT Double Switch Pot Corners',
'GT Double Switch Left', 'GT Double Switch Left - Crystal', 'GT Double Switch Left', 'GT Double Switch Left - Crystal', 'GT Double Switch Entry - Ranged Switches',
'GT Double Switch Entry - Ranged Switches', 'GT Double Switch Exit', 'GT Spike Crystal Left', 'GT Double Switch Exit', 'GT Spike Crystal Left',
'GT Spike Crystal Right', 'GT Warp Maze - Left Section', 'GT Warp Maze - Mid Section', 'GT Spike Crystal Right', 'GT Warp Maze - Left Section', 'GT Warp Maze - Mid Section',
'GT Warp Maze - Right Section', 'GT Warp Maze - Pit Section', 'GT Warp Maze - Pit Exit Warp Spot', 'GT Warp Maze - Right Section', 'GT Warp Maze - Pit Section', 'GT Warp Maze - Pit Exit Warp Spot',
'GT Warp Maze Exit Section', 'GT Firesnake Room', 'GT Firesnake Room Ledge', 'GT Warp Maze - Rail Choice', 'GT Warp Maze Exit Section', 'GT Firesnake Room', 'GT Firesnake Room Ledge', 'GT Warp Maze - Rail Choice',
+10 -6
View File
@@ -1448,9 +1448,8 @@ def junk_fill_inaccessible(world, player):
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world) base_world = copy_world(world, True)
base_world.override_bomb_check = True base_world.override_bomb_check = True
world.key_logic = {}
# remove regions that have a dungeon entrance # remove regions that have a dungeon entrance
accessible_regions = list() accessible_regions = list()
@@ -1617,9 +1616,8 @@ def build_accessible_entrance_list(world, start_region, player, assumed_inventor
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world) base_world = copy_world(world, True)
base_world.override_bomb_check = True base_world.override_bomb_check = True
world.key_logic = {}
connect_simple(base_world, 'Links House S&Q', start_region, player) connect_simple(base_world, 'Links House S&Q', start_region, player)
blank_state = CollectionState(base_world) blank_state = CollectionState(base_world)
@@ -1727,9 +1725,8 @@ def can_reach(world, entrance_name, region_name, player):
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world) base_world = copy_world(world, True)
base_world.override_bomb_check = True base_world.override_bomb_check = True
world.key_logic = {}
entrance = world.get_entrance(entrance_name, player) entrance = world.get_entrance(entrance_name, player)
connect_simple(base_world, 'Links House S&Q', entrance.parent_region.name, player) connect_simple(base_world, 'Links House S&Q', entrance.parent_region.name, player)
@@ -2059,6 +2056,8 @@ mandatory_connections = [('Old Man S&Q', 'Old Man House'),
('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'), ('Lost Woods Hideout (top to bottom)', 'Lost Woods Hideout (bottom)'),
('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'), ('Lumberjack Tree (top to bottom)', 'Lumberjack Tree (bottom)'),
('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'), ('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'),
('Kakariko Well (top to back)', 'Kakariko Well (back)'),
('Blinds Hideout N', 'Blinds Hideout (Top)'),
('Bat Cave Door', 'Bat Cave (left)'), ('Bat Cave Door', 'Bat Cave (left)'),
('Sewer Drop', 'Sewers Rat Path'), ('Sewer Drop', 'Sewers Rat Path'),
('Old Man Cave Dropdown', 'Old Man Cave'), ('Old Man Cave Dropdown', 'Old Man Cave'),
@@ -2066,10 +2065,13 @@ mandatory_connections = [('Old Man S&Q', 'Old Man House'),
('Old Man House Back to Front', 'Old Man House'), ('Old Man House Back to Front', 'Old Man House'),
('Spectacle Rock Cave Drop', 'Spectacle Rock Cave (Bottom)'), ('Spectacle Rock Cave Drop', 'Spectacle Rock Cave (Bottom)'),
('Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave (Bottom)'), ('Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave (Bottom)'),
('Death Mountain Return Cave E', 'Death Mountain Return Cave (right)'),
('Death Mountain Return Cave W', 'Death Mountain Return Cave (left)'),
('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'), ('Spiral Cave (top to bottom)', 'Spiral Cave (Bottom)'),
('Light World Death Mountain Shop', 'Light World Death Mountain Shop'), ('Light World Death Mountain Shop', 'Light World Death Mountain Shop'),
('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'), ('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'),
('Paradox Cave Push Block', 'Paradox Cave Front'), ('Paradox Cave Push Block', 'Paradox Cave Front'),
('Paradox Cave Chest Area NE', 'Paradox Cave Bomb Area'),
('Paradox Cave Bomb Jump', 'Paradox Cave'), ('Paradox Cave Bomb Jump', 'Paradox Cave'),
('Paradox Cave Drop', 'Paradox Cave Chest Area'), ('Paradox Cave Drop', 'Paradox Cave Chest Area'),
('Fairy Ascension Cave Climb', 'Fairy Ascension Cave (Top)'), ('Fairy Ascension Cave Climb', 'Fairy Ascension Cave (Top)'),
@@ -2081,6 +2083,8 @@ mandatory_connections = [('Old Man S&Q', 'Old Man House'),
('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'), ('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'),
('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'), ('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'),
('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'), ('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'),
('Hookshot Cave Bonk Path', 'Hookshot Cave (Bonk Islands)'),
('Hookshot Cave Hook Path', 'Hookshot Cave (Hook Islands)'),
('Ganon Drop', 'Bottom of Pyramid') ('Ganon Drop', 'Bottom of Pyramid')
] ]
+111 -5
View File
@@ -2,11 +2,13 @@ import RaceRandom as random
import collections import collections
import itertools import itertools
import logging import logging
import math
from BaseClasses import CollectionState, FillError from BaseClasses import CollectionState, FillError, LocationType
from Items import ItemFactory from Items import ItemFactory
from Regions import shop_to_location_table, retro_shops from Regions import shop_to_location_table, retro_shops
from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback
from source.item.FillUtil import filter_pot_locations, valid_pot_items
def get_dungeon_item_pool(world): def get_dungeon_item_pool(world):
@@ -350,6 +352,25 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# get items to distribute # get items to distribute
classify_major_items(world) classify_major_items(world)
# handle pot shuffle
pots_used = False
pot_item_pool = collections.defaultdict(list)
for item in world.itempool:
if item.name in ['Chicken', 'Big Magic']: # can only fill these in that players world
pot_item_pool[item.player].append(item)
for player, pot_pool in pot_item_pool.items():
if pot_pool:
for pot_item in pot_pool:
world.itempool.remove(pot_item)
pot_locations = [location for location in fill_locations
if location.type == LocationType.Pot and location.player == player]
pot_locations = filter_pot_locations(pot_locations, world)
fast_fill_helper(world, pot_pool, pot_locations)
pots_used = True
if pots_used:
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
random.shuffle(world.itempool) random.shuffle(world.itempool)
progitempool = [item for item in world.itempool if item.advancement] progitempool = [item for item in world.itempool if item.advancement]
prioitempool = [item for item in world.itempool if not item.advancement and item.priority] prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
@@ -360,12 +381,24 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# fill in gtower locations with trash first # fill in gtower locations with trash first
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed' or world.logic[player] in ['owglitches', 'nologic']: if (not gftower_trash or not world.ganonstower_vanilla[player]
or world.logic[player] in ['owglitches', 'nologic']):
continue continue
max_trash = 8 if world.algorithm == 'dungeon_only' else 15 gt_count, total_count = calc_trash_locations(world, player)
gftower_trash_count = (random.randint(15, 50) if world.goal[player] in ['triforcehunt', 'trinity'] else random.randint(0, max_trash)) scale_factor = .75 * (world.crystals_needed_for_gt[player] / 7)
if world.algorithm == 'dungeon_only':
reserved_space = sum(1 for i in progitempool+prioitempool if i.player == player)
max_trash = max(0, min(gt_count, total_count - reserved_space))
else:
max_trash = gt_count
scaled_trash = math.floor(max_trash * scale_factor)
if world.goal[player] in ['triforcehunt', 'trinity']:
gftower_trash_count = random.randint(scaled_trash, max_trash)
else:
gftower_trash_count = random.randint(0, scaled_trash)
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player] gtower_locations = [location for location in fill_locations if location.parent_region.dungeon
and location.parent_region.dungeon.name == 'Ganons Tower' and location.player == player]
random.shuffle(gtower_locations) random.shuffle(gtower_locations)
trashcnt = 0 trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count: while gtower_locations and restitempool and trashcnt < gftower_trash_count:
@@ -415,6 +448,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
fill_locations.remove(l) fill_locations.remove(l)
filtered_fill(world, placeholder_items, placeholder_locations) filtered_fill(world, placeholder_items, placeholder_locations)
if world.players > 1:
fast_fill_pot_for_multiworld(world, restitempool, fill_locations)
if world.algorithm == 'vanilla_fill': if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, restitempool, fill_locations) fast_vanilla_fill(world, restitempool, fill_locations)
else: else:
@@ -424,6 +459,55 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
unfilled = [location.name for location in fill_locations] unfilled = [location.name for location in fill_locations]
if unplaced or unfilled: if unplaced or unfilled:
logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled)
ensure_good_pots(world)
def calc_trash_locations(world, player):
total_count, gt_count = 0, 0
for loc in world.get_locations():
if (loc.player == player and loc.item is None
and (loc.type not in {LocationType.Pot, LocationType.Drop, LocationType.Normal} or not loc.forced_item)
and (loc.type != LocationType.Shop or world.shopsanity[player])
and loc.parent_region.dungeon):
total_count += 1
if loc.parent_region.dungeon.name == 'Ganons Tower':
gt_count += 1
return gt_count, total_count
def ensure_good_pots(world, write_skips=False):
for loc in world.get_locations():
# convert Arrows 5 and Nothing when necessary
if (loc.item.name in {'Arrows (5)', 'Nothing'}
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# can be placed here by multiworld balancing or shop balancing
# change it to something normal for the player it got swapped to
elif (loc.item.name in {'Chicken', 'Big Magic'}
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
if loc.type == LocationType.Pot:
loc.item.player = loc.player
else:
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player)
# do the arrow retro check
if world.retro[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
loc.item = ItemFactory('Rupees (5)', loc.item.player)
# don't write out all pots to spoiler
if write_skips:
if loc.type == LocationType.Pot and loc.item.name in valid_pot_items:
loc.skip = True
invalid_location_replacement = {'Arrows (5)': 'Arrows (10)', 'Nothing': 'Rupees (5)',
'Chicken': 'Rupees (5)', 'Big Magic': 'Small Magic'}
def fast_fill_helper(world, item_pool, fill_locations):
if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, item_pool, fill_locations)
else:
fast_fill(world, item_pool, fill_locations)
# todo: other fast fill methods?
def fast_fill(world, item_pool, fill_locations): def fast_fill(world, item_pool, fill_locations):
@@ -433,6 +517,28 @@ def fast_fill(world, item_pool, fill_locations):
world.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
pot_item_pool = collections.defaultdict(list)
pot_fill_locations = collections.defaultdict(list)
for item in item_pool:
if item.name in valid_pot_items:
pot_item_pool[item.player].append(item)
for loc in fill_locations:
if loc.type == LocationType.Pot:
pot_fill_locations[loc.player].append(loc)
for player in range(1, world.players+1):
flex = 256 - world.pot_contents[player].multiworld_count
fill_count = len(pot_fill_locations[player]) - flex
if fill_count > 0:
fill_spots = random.sample(pot_fill_locations[player], fill_count)
fill_items = random.sample(pot_item_pool[player], fill_count)
for x in fill_items:
item_pool.remove(x)
for x in fill_spots:
fill_locations.remove(x)
fast_fill(world, fill_items, fill_spots)
def filtered_fill(world, item_pool, fill_locations): def filtered_fill(world, item_pool, fill_locations):
while item_pool and fill_locations: while item_pool and fill_locations:
item_to_place = item_pool.pop() item_to_place = item_pool.pop()
+5 -1
View File
@@ -1,3 +1,7 @@
if __name__ == '__main__':
from source.meta.check_requirements import check_requirements
check_requirements()
import json import json
import os import os
import sys import sys
@@ -106,7 +110,7 @@ def guiMain(args=None):
self.pages["startinventory"] = ttk.Frame(self.notebook) self.pages["startinventory"] = ttk.Frame(self.notebook)
self.pages["custom"] = ttk.Frame(self.notebook) self.pages["custom"] = ttk.Frame(self.notebook)
self.notebook.add(self.pages["randomizer"], text='Randomize') self.notebook.add(self.pages["randomizer"], text='Randomize')
self.notebook.add(self.pages["adjust"], text='Adjust') self.notebook.add(self.pages["adjust"], text='Adjust/Patch')
self.notebook.add(self.pages["startinventory"], text='Starting Inventory') self.notebook.add(self.pages["startinventory"], text='Starting Inventory')
self.notebook.add(self.pages["custom"], text='Custom Item Pool') self.notebook.add(self.pages["custom"], text='Custom Item Pool')
self.notebook.pack() self.notebook.pack()
+256
View File
@@ -0,0 +1,256 @@
from dataclasses import dataclass, field
from typing import List
from BaseClasses import CollectionState
from Utils import count_set_bits
SRAM_SIZE = 0x500
ROOM_DATA = 0x000
OVERWORLD_DATA = 0x280
def _new_default_sram():
sram_buf = [0x00] * 0x500
sram_buf[ROOM_DATA+0x20D] = 0xF0
sram_buf[ROOM_DATA+0x20F] = 0xF0
sram_buf[0x379] = 0x68
sram_buf[0x401] = 0xFF
sram_buf[0x402] = 0xFF
return sram_buf
@dataclass
class InitialSram:
_initial_sram_bytes: List[int] = field(default_factory=_new_default_sram)
def _set_value(self, idx: int, val:int):
if idx > SRAM_SIZE:
raise IndexError('SRAM index out of bounds: {idx}')
if not (-1 < val < 256):
raise ValueError('SRAM value must be between 0 and 255: {val}')
self._initial_sram_bytes[idx] = val
def _or_value(self, idx: int, val:int):
if idx > SRAM_SIZE:
raise IndexError('SRAM index out of bounds: {idx}')
if not (-1 < val < 256):
raise ValueError('SRAM value must be between 0 and 255: {val}')
self._initial_sram_bytes[idx] |= val
def pre_open_aga_curtains(self):
self._or_value(ROOM_DATA+0x61, 0x80)
def pre_open_skullwoods_curtains(self):
self._or_value(ROOM_DATA+0x93, 0x80)
def pre_open_lumberjack(self):
self._or_value(OVERWORLD_DATA+0x02, 0x20)
def pre_open_castle_gate(self):
self._or_value(OVERWORLD_DATA+0x1B, 0x20)
def pre_open_ganons_tower(self):
self._or_value(OVERWORLD_DATA+0x43, 0x20)
def pre_open_pyramid_hole(self):
self._or_value(OVERWORLD_DATA+0x5B, 0x20)
def pre_set_overworld_flag(self, owid, bitmask):
self._or_value(OVERWORLD_DATA+owid, bitmask)
def set_starting_equipment(self, world: object, player: int):
equip = [0] * (0x340 + 0x4F)
equip[0x36C] = 0x18
equip[0x36D] = 0x18
equip[0x379] = 0x68
if world.bombbag[player]:
starting_max_bombs = 0
else:
starting_max_bombs = 10
starting_max_arrows = 30
starting_bomb_cap_upgrades = 0
starting_arrow_cap_upgrades = 0
starting_bombs = 0
starting_arrows = 0
startingstate = CollectionState(world)
if startingstate.has('Bow', player):
equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro[player]:
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
equip[0x38E] |= 0x40
if startingstate.has('Titans Mitts', player):
equip[0x354] = 2
elif startingstate.has('Power Glove', player):
equip[0x354] = 1
if startingstate.has('Golden Sword', player):
equip[0x359] = 4
elif startingstate.has('Tempered Sword', player):
equip[0x359] = 3
elif startingstate.has('Master Sword', player):
equip[0x359] = 2
elif startingstate.has('Fighter Sword', player):
equip[0x359] = 1
if startingstate.has('Mirror Shield', player):
equip[0x35A] = 3
elif startingstate.has('Red Shield', player):
equip[0x35A] = 2
elif startingstate.has('Blue Shield', player):
equip[0x35A] = 1
if startingstate.has('Red Mail', player):
equip[0x35B] = 2
elif startingstate.has('Blue Mail', player):
equip[0x35B] = 1
if startingstate.has('Magic Upgrade (1/4)', player):
equip[0x37B] = 2
equip[0x36E] = 0x80
elif startingstate.has('Magic Upgrade (1/2)', player):
equip[0x37B] = 1
equip[0x36E] = 0x80
for item in world.precollected_items:
if item.player != player:
continue
if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
'Red Mail', 'Blue Mail', 'Progressive Armor',
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']:
continue
set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2),
'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1),
'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)}
or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02),
'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20),
'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08),
'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20),
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10),
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20),
'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0),
'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08), 'Map (Agahnims Tower)': (0x369, 0x08),
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02),
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10),
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80),
'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04),
'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40),
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01),
'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10),
'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)}
keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F],
'Small Key (Tower of Hera)': [0x386],
'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382],
'Small Key (Thieves Town)': [0x387],
'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381],
'Small Key (Ice Palace)': [0x385],
'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388],
'Small Key (Ganons Tower)': [0x389],
'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]}
bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5,
'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8}
rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300}
bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10}
arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10}
bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10}
arrows = {'Single Arrow': 1, 'Arrows (10)': 10}
if item.name in set_table:
equip[set_table[item.name][0]] = set_table[item.name][1]
elif item.name in or_table:
equip[or_table[item.name][0]] |= or_table[item.name][1]
elif item.name in set_or_table:
equip[set_or_table[item.name][0]] = set_or_table[item.name][1]
equip[set_or_table[item.name][2]] |= set_or_table[item.name][3]
elif item.name in keys:
for address in keys[item.name]:
equip[address] = min(equip[address] + 1, 99)
elif item.name in bottles:
if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit:
equip[0x35C + equip[0x34F]] = bottles[item.name]
equip[0x34F] += 1
elif item.name in rupees:
equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
elif item.name in bomb_caps:
starting_bomb_cap_upgrades += bomb_caps[item.name]
elif item.name in arrow_caps:
starting_arrow_cap_upgrades += arrow_caps[item.name]
elif item.name in bombs:
starting_bombs += bombs[item.name]
elif item.name in arrows:
if world.retro[player]:
equip[0x38E] |= 0x80
starting_arrows = 1
else:
starting_arrows += arrows[item.name]
elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']:
if item.name == 'Piece of Heart':
equip[0x36B] = (equip[0x36B] + 1) % 4
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
else:
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
equip[0x370] = min(starting_bomb_cap_upgrades, 50)
equip[0x371] = min(starting_arrow_cap_upgrades, 70)
equip[0x343] = min(starting_bombs, (equip[0x370] + starting_max_bombs))
equip[0x377] = min(starting_arrows, (equip[0x371] + starting_max_arrows))
# Assertion and copy equip to initial_sram_bytes
assert equip[:0x340] == [0] * 0x340
self._initial_sram_bytes[0x340:0x38F] = equip[0x340:0x38F]
# Set counters and highest equipment values
self._initial_sram_bytes[0x471] = count_set_bits(self._initial_sram_bytes[0x37A])
self._initial_sram_bytes[0x429] = count_set_bits(self._initial_sram_bytes[0x374])
self._initial_sram_bytes[0x417] = self._initial_sram_bytes[0x359]
self._initial_sram_bytes[0x422] = self._initial_sram_bytes[0x35A]
self._initial_sram_bytes[0x46E] = self._initial_sram_bytes[0x35B]
if world.swords[player] == "swordless":
self._initial_sram_bytes[0x359] = 0xFF
self._initial_sram_bytes[0x417] = 0x00
def set_starting_rupees(self, rupees: int):
if not (-1 < rupees < 10000):
raise ValueError("Starting rupees must be between 0 and 9999")
self._initial_sram_bytes[0x362] = self._initial_sram_bytes[0x360] = rupees & 0xFF
self._initial_sram_bytes[0x363] = self._initial_sram_bytes[0x361] = rupees >> 8
def set_progress_indicator(self, indicator: int):
self._set_value(0x3C5, indicator)
def set_progress_flags(self, flags: int):
self._set_value(0x3C6, flags)
def set_starting_entrance(self, entrance: int):
self._set_value(0x3C8, entrance)
def set_starting_timer(self, seconds: int):
timer = (seconds * 60).to_bytes(4, "little")
self._initial_sram_bytes[0x454] = timer[0]
self._initial_sram_bytes[0x455] = timer[1]
self._initial_sram_bytes[0x456] = timer[2]
self._initial_sram_bytes[0x457] = timer[3]
def set_swordless_curtains(self):
self._or_value(ROOM_DATA+0x61, 0x80)
self._or_value(ROOM_DATA+0x93, 0x80)
def get_initial_sram(self):
assert len(self._initial_sram_bytes) == SRAM_SIZE
return self._initial_sram_bytes[:]
+102 -27
View File
@@ -3,13 +3,14 @@ import logging
import math import math
import RaceRandom as random import RaceRandom as random
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem
from EntranceShuffle import connect_entrance from EntranceShuffle import connect_entrance
from Regions import shop_to_location_table, retro_shops, shop_table_by_location from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location
from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool
from PotShuffle import vanilla_pots
from Items import ItemFactory from Items import ItemFactory
from source.item.FillUtil import trash_items from source.item.FillUtil import trash_items, pot_items
import source.classes.constants as CONST import source.classes.constants as CONST
@@ -43,6 +44,7 @@ Difficulty = namedtuple('Difficulty',
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit']) 'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
total_items_to_place = 153 total_items_to_place = 153
max_goal = 850
difficulties = { difficulties = {
'normal': Difficulty( 'normal': Difficulty(
@@ -203,6 +205,7 @@ def generate_itempool(world, player):
world.push_item(loc, ItemFactory('Triforce', player), False) world.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True loc.event = True
loc.locked = True loc.locked = True
loc.forced_item = loc.item
world.get_location('Ganon', player).event = True world.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True world.get_location('Ganon', player).locked = True
@@ -254,6 +257,9 @@ def generate_itempool(world, player):
world.push_item(world.get_location('Ice Block Drop', player), ItemFactory('Convenient Block', player), False) world.push_item(world.get_location('Ice Block Drop', player), ItemFactory('Convenient Block', player), False)
world.get_location('Ice Block Drop', player).event = True world.get_location('Ice Block Drop', player).event = True
world.get_location('Ice Block Drop', player).locked = True world.get_location('Ice Block Drop', player).locked = True
world.push_item(world.get_location('Skull Star Tile', player), ItemFactory('Hidden Pits', player), False)
world.get_location('Skull Star Tile', player).event = True
world.get_location('Skull Star Tile', player).locked = True
if world.mode[player] == 'standard': if world.mode[player] == 'standard':
world.push_item(world.get_location('Zelda Pickup', player), ItemFactory('Zelda Herself', player), False) world.push_item(world.get_location('Zelda Pickup', player), ItemFactory('Zelda Herself', player), False)
world.get_location('Zelda Pickup', player).event = True world.get_location('Zelda Pickup', player).event = True
@@ -342,8 +348,10 @@ def generate_itempool(world, player):
if clock_mode is not None: if clock_mode is not None:
world.clock_mode = clock_mode world.clock_mode = clock_mode
if world.goal[player] in ['triforcehunt', 'trinity']: goal = world.goal[player]
world.treasure_hunt_count[player], world.treasure_hunt_total[player] = set_default_triforce(world.goal[player], world.treasure_hunt_count[player], world.treasure_hunt_total[player]) if goal in ['triforcehunt', 'trinity']:
g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player])
world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t
world.treasure_hunt_icon[player] = 'Triforce Piece' world.treasure_hunt_icon[player] = 'Triforce Piece'
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
@@ -393,11 +401,16 @@ def generate_itempool(world, player):
if world.retro[player]: if world.retro[player]:
set_up_take_anys(world, player) set_up_take_anys(world, player)
if world.keydropshuffle[player]: if world.dropshuffle[player]:
world.itempool += [ItemFactory('Small Key (Universal)', player)] * 32 world.itempool += [ItemFactory('Small Key (Universal)', player)] * 13
if world.pottery[player] not in ['none', 'cave']:
world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19
create_dynamic_shop_locations(world, player) create_dynamic_shop_locations(world, player)
if world.pottery[player] not in ['none', 'keys']:
add_pot_contents(world, player)
take_any_locations = [ take_any_locations = [
'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut',
@@ -418,7 +431,9 @@ def set_up_take_anys(world, player):
if 'Archery Game' in take_any_locations: if 'Archery Game' in take_any_locations:
take_any_locations.remove('Archery Game') take_any_locations.remove('Archery Game')
regions = random.sample(take_any_locations, 5) take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0]
regions = random.sample(take_any_candidates, 5)
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player) old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
world.regions.append(old_man_take_any) world.regions.append(old_man_take_any)
@@ -551,7 +566,7 @@ def set_up_shops(world, player):
removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player] removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player]
for remove in removals: for remove in removals:
world.itempool.remove(remove) world.itempool.remove(remove)
world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade
else: else:
cap_shop = world.get_region('Capacity Upgrade', player).shop cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbag cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbag
@@ -754,17 +769,25 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)'
'Rupees (100)': 100, 'Rupees (300)': 300} 'Rupees (100)': 100, 'Rupees (300)': 300}
def add_pot_contents(world, player):
for super_tile, pot_list in vanilla_pots.items():
for pot in pot_list:
if pot.item not in [PotItem.Hole, PotItem.Key, PotItem.Switch]:
if valid_pot_location(pot, world.pot_pool[player], world, player):
item = ('Rupees (5)' if world.retro[player] and pot_items[pot.item] == 'Arrows (5)'
else pot_items[pot.item])
world.itempool.append(ItemFactory(item, player))
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, flute_activated): def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, flute_activated):
pool = [] pool = []
placed_items = {} placed_items = {}
precollected_items = [] precollected_items = []
clock_mode = None clock_mode = None
if treasure_hunt_total == 0: if treasure_hunt_total == 0 and goal in ['triforcehunt', 'trinity']:
if goal == 'triforcehunt': treasure_hunt_total = 30 if goal == 'triforcehunt' else 10
treasure_hunt_total = 30 # triforce pieces max out
elif goal == 'trinity': triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal)
treasure_hunt_total = 10
triforcepool = ['Triforce Piece'] * int(treasure_hunt_total)
pool.extend(alwaysitems) pool.extend(alwaysitems)
@@ -793,7 +816,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
lamps_needed_for_dark_rooms = 1 lamps_needed_for_dark_rooms = 1
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start # insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
if shuffle == 'insanity_legacy': if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror') place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl') place_item('Sanctuary', 'Moon Pearl')
else: else:
@@ -851,6 +874,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
place_item('Master Sword Pedestal', swords_to_use.pop()) place_item('Master Sword Pedestal', swords_to_use.pop())
else: else:
place_item('Master Sword Pedestal', 'Triforce') place_item('Master Sword Pedestal', 'Triforce')
pool.append(swords_to_use.pop())
else: else:
pool.extend(diff.progressivesword if want_progressives() else diff.basicsword) pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
if swords == 'assured': if swords == 'assured':
@@ -862,26 +886,19 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
pool.remove('Fighter Sword') pool.remove('Fighter Sword')
pool.extend(['Rupees (50)']) pool.extend(['Rupees (50)'])
extraitems = total_items_to_place - len(pool) - len(placed_items)
if timer in ['timed', 'timed-countdown']: if timer in ['timed', 'timed-countdown']:
pool.extend(diff.timedother) pool.extend(diff.timedother)
extraitems -= len(diff.timedother)
clock_mode = 'stopwatch' if timer == 'timed' else 'countdown' clock_mode = 'stopwatch' if timer == 'timed' else 'countdown'
elif timer == 'timed-ohko': elif timer == 'timed-ohko':
pool.extend(diff.timedohko) pool.extend(diff.timedohko)
extraitems -= len(diff.timedohko)
clock_mode = 'countdown-ohko' clock_mode = 'countdown-ohko'
if goal in ['triforcehunt', 'trinity']: if goal in ['triforcehunt', 'trinity']:
pool.extend(triforcepool) pool.extend(triforcepool)
extraitems -= len(triforcepool)
for extra in diff.extras: for extra in diff.extras:
if extraitems > 0: pool.extend(extra)
if len(extra) > extraitems:
extra = random.choices(extra, k=extraitems) # note: massage item pool now handles shrinking the pool appropriately
pool.extend(extra)
extraitems -= len(extra)
if goal in ['pedestal', 'trinity'] and swords != 'vanilla': if goal in ['pedestal', 'trinity'] and swords != 'vanilla':
place_item('Master Sword Pedestal', 'Triforce') place_item('Master Sword Pedestal', 'Triforce')
@@ -906,6 +923,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
pool.extend(['Small Key (Universal)']) pool.extend(['Small Key (Universal)'])
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) 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, bombbag, customitemarray): def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray):
pool = [] pool = []
placed_items = {} placed_items = {}
@@ -932,7 +950,8 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
# Triforce Pieces # Triforce Pieces
if goal in ['triforcehunt', 'trinity']: if goal in ['triforcehunt', 'trinity']:
customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"])
customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t
itemtotal = 0 itemtotal = 0
# Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors # Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors
@@ -964,7 +983,15 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
pool.append(thisbottle) pool.append(thisbottle)
if customitemarray["triforcepieces"] > 0 or customitemarray["triforcepiecesgoal"] > 0: if customitemarray["triforcepieces"] > 0 or customitemarray["triforcepiecesgoal"] > 0:
# Location pool doesn't support larger values
treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1)
treasure_hunt_icon = 'Triforce Piece' treasure_hunt_icon = 'Triforce Piece'
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity'])
and (customitemarray["triforce"] == 0)):
extrapieces = treasure_hunt_count - customitemarray["triforcepieces"]
pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces
if timer in ['display', 'timed', 'timed-countdown']: if timer in ['display', 'timed', 'timed-countdown']:
clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch' clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch'
@@ -1020,6 +1047,54 @@ def set_default_triforce(goal, custom_goal, custom_total):
triforce_total = max(min(custom_total, 128), triforce_goal) #128 max to ensure other progression can fit. triforce_total = max(min(custom_total, 128), triforce_goal) #128 max to ensure other progression can fit.
return (triforce_goal, triforce_total) return (triforce_goal, triforce_total)
def make_customizer_pool(world, player):
pool = []
placed_items = {}
precollected_items = []
clock_mode = None
def place_item(loc, item):
assert loc not in placed_items
placed_items[loc] = item
diff = difficulties[world.difficulty[player]]
for item_name, amount in world.customizer.get_item_pool()[player].items():
if isinstance(amount, int):
if item_name == 'Bottle (Random)':
for _ in range(amount):
pool.append(random.choice(diff.bottles))
else:
pool.extend([item_name] * amount)
timer = world.timer[player]
if timer in ['display', 'timed', 'timed-countdown']:
clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch'
elif timer == 'timed-ohko':
clock_mode = 'countdown-ohko'
elif timer == 'ohko':
clock_mode = 'ohko'
if world.goal[player] == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
return pool, placed_items, precollected_items, clock_mode, 1
# location pool doesn't support larger values at this time
def set_default_triforce(goal, custom_goal, custom_total):
triforce_goal, triforce_total = 0, 0
if goal == 'triforcehunt':
triforce_goal, triforce_total = 20, 30
elif goal == 'trinity':
triforce_goal, triforce_total = 8, 10
if custom_goal > 0:
triforce_goal = max(min(custom_goal, max_goal), 1)
if custom_total > 0:
triforce_total = max(min(custom_total, max_goal), triforce_goal)
return triforce_goal, triforce_total
# A quick test to ensure all combinations generate the correct amount of items. # A quick test to ensure all combinations generate the correct amount of items.
def test(): def test():
for difficulty in ['normal', 'hard', 'expert']: for difficulty in ['normal', 'hard', 'expert']:
+47 -41
View File
@@ -27,7 +27,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche
'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'), 'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
'Hammer': (True, False, None, 0x09, 250, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the Hammer'), 'Hammer': (True, False, None, 0x09, 250, 'Stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the Hammer'),
'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), 'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'), 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'),
'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'), 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'),
@@ -38,65 +38,70 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche
'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'), 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'),
'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'), 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'),
'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'), 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'),
'Magic Powder': (True, False, None, 0x0D, 50, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'), 'Magic Powder': (True, False, None, 0x0D, 50, 'You can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'),
'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'),
'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'), 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'),
'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'),
'Flippers': (True, False, None, 0x1E, 250, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'), 'Flippers': (True, False, None, 0x1E, 250, 'Fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'),
'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'),
'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'),
'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
'Ether': (True, False, None, 0x10, 100, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), 'Ether': (True, False, None, 0x10, 100, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
'Quake': (True, False, None, 0x11, 100, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), 'Quake': (True, False, None, 0x11, 100, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
'Bottle': (True, False, None, 0x16, 50, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'), 'Bottle': (True, False, None, 0x16, 50, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'),
'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'), 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'),
'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a Bottle'), 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'),
'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a Bottle'), 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'),
'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'), 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'),
'Bottle (Bee)': (True, False, None, 0x3C, 50, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a Bottle'), 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'),
'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a Bottle'), 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'),
'Master Sword': (True, False, 'Sword', 0x50, 100, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), 'Master Sword': (True, False, 'Sword', 0x50, 100, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'),
'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'),
'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Fighter Sword'), 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Fighter Sword'),
'Sword and Shield': (True, False, 'Sword', 0x00, 'An uncle\nsword rests\nhere!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'),
'Golden Sword': (True, False, 'Sword', 0x03, 200, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'),
'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'), 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'A better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'),
'Progressive Glove': (True, False, None, 0x61, 150, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'), 'Progressive Glove': (True, False, None, 0x61, 150, 'A way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'),
'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Arrows'), 'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Arrows'),
'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01], 999, None, None, None, None, None, None, None), 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x08], 999, None, None, None, None, None, None, None),
'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02], 999, None, None, None, None, None, None, None), 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x09], 999, None, None, None, None, None, None, None),
'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03], 999, None, None, None, None, None, None, None), 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x0a], 999, None, None, None, None, None, None, None),
'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), 'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
'Power Star': (True, False, None, 0x6B, 100, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), 'Power Star': (True, False, None, 0x6B, 100, 'A small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
'Triforce Piece': (True, False, None, 0x6C, 100, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), 'Triforce Piece': (True, False, None, 0x6C, 100, 'A small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'),
'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x01], 999, None, None, None, None, None, None, None),
'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x02], 999, None, None, None, None, None, None, None),
'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x03], 999, None, None, None, None, None, None, None),
'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x04], 999, None, None, None, None, None, None, None),
'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x05], 999, None, None, None, None, None, None, None),
'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x06], 999, None, None, None, None, None, None, None),
'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x07], 999, None, None, None, None, None, None, None),
'Single Arrow': (False, False, None, 0x43, 3, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), 'Single Arrow': (False, False, None, 0x43, 3, 'A lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), 'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'),
'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), 'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), 'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'),
'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'),
'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'),
'Big Magic': (False, False, None, 0x5A, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'),
'Chicken': (False, False, None, 0x5A, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'),
'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), 'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), 'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
'Blue Mail': (False, True, None, 0x22, 50, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'), 'Blue Mail': (False, True, None, 0x22, 50, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'),
'Red Mail': (False, True, None, 0x23, 100, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'), 'Red Mail': (False, True, None, 0x23, 100, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'),
'Progressive Armor': (False, True, None, 0x60, 50, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), 'Progressive Armor': (False, True, None, 0x60, 50, 'Time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'),
'Blue Boomerang': (True, False, None, 0x0C, 50, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the Blue Boomerang'), 'Blue Boomerang': (True, False, None, 0x0C, 50, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the Blue Boomerang'),
'Red Boomerang': (True, False, None, 0x2A, 50, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the Red Boomerang'), 'Red Boomerang': (True, False, None, 0x2A, 50, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the Red Boomerang'),
'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'the Blue Shield'), 'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a Blue Shield'),
'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'the Red Shield'), 'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'a Red Shield'),
'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), 'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'),
'Progressive Shield': (True, False, None, 0x5F, 50, 'have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), 'Progressive Shield': (True, False, None, 0x5F, 50, 'Have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'),
'Bug Catching Net': (True, False, None, 0x21, 50, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the Bug Net'), 'Bug Catching Net': (True, False, None, 0x21, 50, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the Bug Net'),
'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the Blue Cane'), 'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the Blue Cane'),
'Boss Heart Container': (False, False, None, 0x3E, 40, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'),
'Sanctuary Heart Container': (False, False, None, 0x3F, 50, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'),
'Piece of Heart': (False, False, None, 0x17, 10, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'), 'Piece of Heart': (False, False, None, 0x17, 10, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'),
'Rupee (1)': (False, False, None, 0x34, 0, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a green rupee'), 'Rupee (1)': (False, False, None, 0x34, 0, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a green rupee'),
'Rupees (5)': (False, False, None, 0x35, 2, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'), 'Rupees (5)': (False, False, None, 0x35, 2, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'),
@@ -104,12 +109,12 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche
'Rupees (50)': (False, False, None, 0x41, 25, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'), 'Rupees (50)': (False, False, None, 0x41, 25, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'),
'Rupees (100)': (False, False, None, 0x40, 50, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'), 'Rupees (100)': (False, False, None, 0x40, 50, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'),
'Rupees (300)': (False, False, None, 0x46, 150, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'), 'Rupees (300)': (False, False, None, 0x46, 150, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'),
'Rupoor': (False, False, None, 0x59, 0, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), 'Rupoor': (False, False, None, 0x59, 0, 'A debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'),
'Red Clock': (False, True, None, 0x5B, 0, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), 'Red Clock': (False, True, None, 0x5B, 0, 'A waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'),
'Blue Clock': (False, True, None, 0x5C, 50, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), 'Blue Clock': (False, True, None, 0x5C, 50, 'A bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'),
'Green Clock': (False, True, None, 0x5D, 200, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), 'Green Clock': (False, True, None, 0x5D, 200, 'A lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'),
'Single RNG': (False, True, None, 0x62, 300, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'), 'Single RNG': (False, True, None, 0x62, 300, 'Something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'),
'Multi RNG': (False, True, None, 0x63, 100, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), 'Multi RNG': (False, True, None, 0x63, 100, 'Something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'),
'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Eastern Palace'), 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a Small Key to Eastern Palace'),
@@ -188,6 +193,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche
'Maiden Rescued': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Maiden Rescued': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
'Maiden Unmasked': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Maiden Unmasked': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
'Convenient Block': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Convenient Block': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
'Hidden Pits': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
'Zelda Herself': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Zelda Herself': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
'Zelda Delivered': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Zelda Delivered': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
} }
+66 -34
View File
@@ -13,7 +13,7 @@ from Bosses import place_bosses
from Items import ItemFactory from Items import ItemFactory
from KeyDoorShuffle import validate_key_placement from KeyDoorShuffle import validate_key_placement
from OverworldGlitchRules import create_owg_connections from OverworldGlitchRules import create_owg_connections
from PotShuffle import shuffle_pots from PotShuffle import shuffle_pots, shuffle_pot_switches
from Regions import create_regions, create_shops, mark_light_world_regions, mark_dark_world_regions, create_dungeon_regions, adjust_locations 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 OWEdges import create_owedges
from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits from OverworldShuffle import link_overworld, update_world_regions, create_flute_exits
@@ -24,15 +24,15 @@ from DoorShuffle import link_doors, connect_portal, link_doors_prep
from RoomData import create_rooms from RoomData import create_rooms
from Rules import set_rules from Rules import set_rules
from Dungeons import create_dungeons from Dungeons import create_dungeons
from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_pots
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config
from source.tools.BPS import create_bps_from_data
__version__ = '1.0.1.0-u'
__version__ = '1.0.0.3-u'
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
@@ -100,18 +100,21 @@ def main(args, seed=None, fish=None):
world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
world.experimental = args.experimental.copy() world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
world.potshuffle = args.shufflepots.copy()
world.fish = fish world.fish = fish
world.shopsanity = args.shopsanity.copy() world.shopsanity = args.shopsanity.copy()
world.keydropshuffle = args.keydropshuffle.copy() world.dropshuffle = args.dropshuffle.copy()
world.pottery = args.pottery.copy()
world.potshuffle = args.shufflepots.copy()
world.mixed_travel = args.mixed_travel.copy() world.mixed_travel = args.mixed_travel.copy()
world.standardize_palettes = args.standardize_palettes.copy() world.standardize_palettes = args.standardize_palettes.copy()
world.treasure_hunt_count = args.triforce_goal.copy() world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()}
world.treasure_hunt_total = args.triforce_pool.copy() world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()}
world.shufflelinks = args.shufflelinks.copy() world.shufflelinks = args.shufflelinks.copy()
world.pseudoboots = args.pseudoboots.copy() world.pseudoboots = args.pseudoboots.copy()
world.overworld_map = args.overworld_map.copy() world.overworld_map = args.overworld_map.copy()
world.restrict_boss_items = args.restrict_boss_items.copy() world.restrict_boss_items = args.restrict_boss_items.copy()
world.collection_rate = args.collection_rate.copy()
world.colorizepots = args.colorizepots.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -147,14 +150,17 @@ def main(args, seed=None, fish=None):
if hasattr(world,"escape_assist") and player in world.escape_assist: if hasattr(world,"escape_assist") and player in world.escape_assist:
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
for tok in filter(None, args.startinventory[player].split(',')): if args.usestartinventory[player]:
item = ItemFactory(tok.strip(), player) for tok in filter(None, args.startinventory[player].split(',')):
if item: item = ItemFactory(tok.strip(), player)
world.push_precollected(item) if item:
world.push_precollected(item)
if args.create_spoiler and not args.jsonout: if args.create_spoiler and not args.jsonout:
logger.info(world.fish.translate("cli","cli","patching.spoiler")) logger.info(world.fish.translate("cli", "cli", "create.meta"))
world.spoiler.meta_to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.meta_to_file(output_path(f'{outfilebase}_Spoiler.txt'))
if args.mystery and not args.suppress_meta:
world.spoiler.mystery_meta_to_file(output_path(f'{outfilebase}_meta.txt'))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
create_regions(world, player) create_regions(world, player)
@@ -173,7 +179,10 @@ def main(args, seed=None, fish=None):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.potshuffle[player]: if world.potshuffle[player]:
shuffle_pots(world, player) if world.pottery[player] in ['none', 'cave', 'keys', 'cavekeys']:
shuffle_pots(world, player)
else:
shuffle_pot_switches(world, player)
logger.info(world.fish.translate("cli","cli","shuffling.overworld")) logger.info(world.fish.translate("cli","cli","shuffling.overworld"))
@@ -273,11 +282,12 @@ def main(args, seed=None, fish=None):
customize_shops(world, player) customize_shops(world, player)
if args.algorithm in ['balanced', 'equitable']: if args.algorithm in ['balanced', 'equitable']:
balance_money_progression(world) balance_money_progression(world)
ensure_good_pots(world, True)
rom_names = [] rom_names = []
jsonout = {} jsonout = {}
enemized = False enemized = False
if not args.suppress_rom: if not args.suppress_rom or args.bps:
logger.info(world.fish.translate("cli","cli","patching.rom")) logger.info(world.fish.translate("cli","cli","patching.rom"))
for team in range(world.teams): for team in range(world.teams):
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
@@ -303,7 +313,7 @@ def main(args, seed=None, fish=None):
logging.warning(enemizerMsg) logging.warning(enemizerMsg)
raise EnemizerError(enemizerMsg) raise EnemizerError(enemizerMsg)
patch_rom(world, rom, player, team, enemized, bool(args.outputname)) patch_rom(world, rom, player, team, enemized, bool(args.mystery))
if args.race: if args.race:
patch_race_rom(rom) patch_race_rom(rom)
@@ -314,7 +324,7 @@ def main(args, seed=None, fish=None):
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[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]) args.shuffle_sfx[player], args.msu_resume[player])
if args.jsonout: if args.jsonout:
jsonout[f'patch_t{team}_p{player}'] = rom.patches jsonout[f'patch_t{team}_p{player}'] = rom.patches
@@ -325,7 +335,14 @@ def main(args, seed=None, fish=None):
if world.players > 1 or world.teams > 1: if world.players > 1 or world.teams > 1:
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else '' outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
outfilesuffix = f'_{Settings.make_code(world, player)}' if not args.outputname else '' outfilesuffix = f'_{Settings.make_code(world, player)}' if not args.outputname else ''
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')) if args.bps:
patchfile = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.bps')
patch = create_bps_from_data(LocalRom(args.rom, patch=False).buffer, rom.buffer)
with open(patchfile, 'wb') as stream:
stream.write(patch.binary_ba)
if not args.suppress_rom:
sfc_file = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(sfc_file)
if world.players > 1: if world.players > 1:
multidata = zlib.compress(json.dumps({"names": parsed_names, multidata = zlib.compress(json.dumps({"names": parsed_names,
@@ -341,9 +358,13 @@ def main(args, seed=None, fish=None):
with open(output_path('%s_multidata' % outfilebase), 'wb') as f: with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
f.write(multidata) f.write(multidata)
if args.mystery and not args.suppress_meta:
world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt'))
elif args.create_spoiler and not args.jsonout:
world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt'))
if args.create_spoiler and not args.jsonout: if args.create_spoiler and not args.jsonout:
logger.info(world.fish.translate("cli","cli","patching.spoiler")) logger.info(world.fish.translate("cli", "cli", "patching.spoiler"))
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path(f'{outfilebase}_Spoiler.txt'))
if not args.skip_playthrough: if not args.skip_playthrough:
logger.info(world.fish.translate("cli","cli","calc.playthrough")) logger.info(world.fish.translate("cli","cli","calc.playthrough"))
@@ -356,8 +377,8 @@ def main(args, seed=None, fish=None):
if args.jsonout: if args.jsonout:
with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile:
outfile.write(world.spoiler.to_json()) outfile.write(world.spoiler.to_json())
else: elif world.players > 1 or world.logic[1] != "nologic":
world.spoiler.playthru_to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.playthrough_to_file(output_path(f'{outfilebase}_Spoiler.txt'))
YES = world.fish.translate("cli","cli","yes") YES = world.fish.translate("cli","cli","yes")
NO = world.fish.translate("cli","cli","no") NO = world.fish.translate("cli","cli","no")
@@ -377,7 +398,7 @@ def main(args, seed=None, fish=None):
return world return world
def copy_world(world): def copy_world(world, partial_copy=False):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, 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.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
@@ -426,7 +447,9 @@ def copy_world(world):
ret.intensity = world.intensity.copy() ret.intensity = world.intensity.copy()
ret.experimental = world.experimental.copy() ret.experimental = world.experimental.copy()
ret.shopsanity = world.shopsanity.copy() ret.shopsanity = world.shopsanity.copy()
ret.keydropshuffle = world.keydropshuffle.copy() ret.dropshuffle = world.dropshuffle.copy()
ret.pottery = world.pottery.copy()
ret.potshuffle = world.potshuffle.copy()
ret.mixed_travel = world.mixed_travel.copy() ret.mixed_travel = world.mixed_travel.copy()
ret.standardize_palettes = world.standardize_palettes.copy() ret.standardize_palettes = world.standardize_palettes.copy()
ret.owswaps = world.owswaps.copy() ret.owswaps = world.owswaps.copy()
@@ -434,8 +457,6 @@ def copy_world(world):
ret.prizes = world.prizes.copy() ret.prizes = world.prizes.copy()
ret.restrict_boss_items = world.restrict_boss_items.copy() ret.restrict_boss_items = world.restrict_boss_items.copy()
ret.exp_cache = world.exp_cache.copy()
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
create_regions(ret, player) create_regions(ret, player)
update_world_regions(ret, player) update_world_regions(ret, player)
@@ -447,6 +468,9 @@ def copy_world(world):
if world.logic[player] in ('owglitches', 'nologic'): if world.logic[player] in ('owglitches', 'nologic'):
create_owg_connections(ret, player) create_owg_connections(ret, player)
# there are region references here they must be migrated to preserve integrity
# ret.exp_cache = world.exp_cache.copy()
copy_dynamic_regions_and_locations(world, ret) copy_dynamic_regions_and_locations(world, ret)
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.mode[player] == 'standard': if world.mode[player] == 'standard':
@@ -495,6 +519,7 @@ def copy_world(world):
new_location.access_rule = lambda state: True new_location.access_rule = lambda state: True
new_location.item_rule = lambda state: True new_location.item_rule = lambda state: True
new_location.forced_item = location.forced_item new_location.forced_item = location.forced_item
new_location.pot = location.pot
# copy remaining itempool. No item in itempool should have an assigned location # copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool: for item in world.itempool:
@@ -519,9 +544,10 @@ def copy_world(world):
ret.dungeon_layouts = world.dungeon_layouts ret.dungeon_layouts = world.dungeon_layouts
ret.key_logic = world.key_logic ret.key_logic = world.key_logic
ret.dungeon_portals = world.dungeon_portals ret.dungeon_portals = world.dungeon_portals
for player, portals in world.dungeon_portals.items(): if not partial_copy:
for portal in portals: for player, portals in world.dungeon_portals.items():
connect_portal(portal, ret, player) for portal in portals:
connect_portal(portal, ret, player)
ret.sanc_portal = world.sanc_portal ret.sanc_portal = world.sanc_portal
from OverworldShuffle import categorize_world_regions from OverworldShuffle import categorize_world_regions
@@ -529,6 +555,10 @@ def copy_world(world):
categorize_world_regions(ret, player) categorize_world_regions(ret, player)
set_rules(ret, player) set_rules(ret, player)
if partial_copy:
# undo some of the things that unintentionally affect the original world object
world.key_logic = {}
return ret return ret
@@ -566,7 +596,7 @@ def create_playthrough(world):
# get locations containing progress items # get locations containing progress items
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement] prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop'] optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile']
state_cache = [None] state_cache = [None]
collection_spheres = [] collection_spheres = []
state = CollectionState(world) state = CollectionState(world)
@@ -672,9 +702,11 @@ def create_playthrough(world):
old_world.spoiler.paths = dict() old_world.spoiler.paths = dict()
for player in range(1, world.players + 1): 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}) if world.logic[player] != 'nologic':
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})
# we can finally output our playthrough # we can finally output our playthrough
old_world.spoiler.playthrough = {"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): for i, sphere in enumerate(collection_spheres):
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere} if world.logic[player] != 'nologic':
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere}
+80 -16
View File
@@ -8,8 +8,10 @@ import shlex
import urllib.parse import urllib.parse
import websockets import websockets
from BaseClasses import PotItem, PotFlags
import Items import Items
import Regions import Regions
import PotShuffle
class ReceivedItem: class ReceivedItem:
@@ -57,8 +59,13 @@ class Context:
self.key_drop_mode = False self.key_drop_mode = False
self.shop_mode = False self.shop_mode = False
self.retro_mode = False self.retro_mode = False
self.pottery_mode = False
self.mystery_mode = False
self.ignore_count = 0 self.ignore_count = 0
self.lookup_name_to_id = {}
self.lookup_id_to_name = {}
def color_code(*args): def color_code(*args):
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, 'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
@@ -83,6 +90,12 @@ INGAME_MODES = {0x07, 0x09, 0x0b}
SAVEDATA_START = WRAM_START + 0xF000 SAVEDATA_START = WRAM_START + 0xF000
SAVEDATA_SIZE = 0x500 SAVEDATA_SIZE = 0x500
POT_ITEMS_SRAM_START = WRAM_START + 0x016018 # 2 bytes per room
SPRITE_ITEMS_SRAM_START = WRAM_START + 0x016268 # 2 bytes per room
SHOP_SRAM_START = WRAM_START + 0x0164B8 # 2 bytes?
ITEM_SRAM_SIZE = 0x250
SHOP_SRAM_LEN = 0x29 # 41 tracked items
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
@@ -92,11 +105,9 @@ SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes?
DYNAMIC_TOTAL_ADDR = SAVEDATA_START + 0x33E # 2 bytes DYNAMIC_TOTAL_ADDR = SAVEDATA_START + 0x33E # 2 bytes
MODE_FLAGS = SAVEDATA_START + 0x33D # 1 byte MODE_FLAGS = SAVEDATA_START + 0x33D # 1 byte
SHOP_SRAM_LEN = 0x29 # 41 tracked items
location_shop_order = [Regions.shop_to_location_table.keys()] + [Regions.retro_shops.keys()] location_shop_order = [Regions.shop_to_location_table.keys()] + [Regions.retro_shops.keys()]
location_shop_ids = {0x0112, 0x0110, 0x010F, 0x00FF, 0x011F, 0x0109, 0x0115} location_shop_ids = {0x0112, 0x0110, 0x010F, 0x00FF, 0x011F, 0x0109, 0x0115}
@@ -349,6 +360,8 @@ location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
'Purple Chest': (0x3c9, 0x10), 'Purple Chest': (0x3c9, 0x10),
"Link's Uncle": (0x3c6, 0x1), "Link's Uncle": (0x3c6, 0x1),
'Hobo': (0x3c9, 0x1)} 'Hobo': (0x3c9, 0x1)}
location_table_pot_items = {}
location_table_sprite_items = {}
SNES_DISCONNECTED = 0 SNES_DISCONNECTED = 0
SNES_CONNECTING = 1 SNES_CONNECTING = 1
@@ -676,7 +689,7 @@ async def process_server_cmd(ctx : Context, cmd, args):
ctx.player_names = {p: n for p, n in args[1]} ctx.player_names = {p: n for p, n in args[1]}
msgs = [] msgs = []
if ctx.locations_checked: if ctx.locations_checked:
msgs.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) msgs.append(['LocationChecks', [ctx.lookup_name_to_id[loc] for loc in ctx.locations_checked]])
if ctx.locations_scouted: if ctx.locations_scouted:
msgs.append(['LocationScouts', list(ctx.locations_scouted)]) msgs.append(['LocationScouts', list(ctx.locations_scouted)])
if msgs: if msgs:
@@ -689,7 +702,7 @@ async def process_server_cmd(ctx : Context, cmd, args):
elif start_index != len(ctx.items_received): elif start_index != len(ctx.items_received):
sync_msg = [['Sync']] sync_msg = [['Sync']]
if ctx.locations_checked: if ctx.locations_checked:
sync_msg.append(['LocationChecks', [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]]) sync_msg.append(['LocationChecks', [ctx.lookup_name_to_id[loc] for loc in ctx.locations_checked]])
await send_msgs(ctx.socket, sync_msg) await send_msgs(ctx.socket, sync_msg)
if start_index == len(ctx.items_received): if start_index == len(ctx.items_received):
for item in items: for item in items:
@@ -701,7 +714,7 @@ async def process_server_cmd(ctx : Context, cmd, args):
if location not in ctx.locations_info: if location not in ctx.locations_info:
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'} replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
item_name = replacements.get(item, get_item_name_from_id(item)) item_name = replacements.get(item, get_item_name_from_id(item))
logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.lookup_id_to_name.keys())[location - 1]}") logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(ctx.lookup_id_to_name.keys())[location - 1]}")
ctx.locations_info[location] = (item, player) ctx.locations_info[location] = (item, player)
ctx.watcher_event.set() ctx.watcher_event.set()
@@ -710,7 +723,8 @@ async def process_server_cmd(ctx : Context, cmd, args):
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green') item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta') player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta') player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location))) location_name = get_location_name_from_address(ctx, location)
logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, location_name))
if cmd == 'Print': if cmd == 'Print':
logging.info(args) logging.info(args)
@@ -779,10 +793,10 @@ async def console_loop(ctx : Context):
for index, item in enumerate(ctx.items_received, 1): for index, item in enumerate(ctx.items_received, 1):
logging.info('%s from %s (%s) (%d/%d in list)' % ( logging.info('%s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
get_location_name_from_address(item.location), index, len(ctx.items_received))) get_location_name_from_address(ctx, item.location), index, len(ctx.items_received)))
if command[0] == '/missing': if command[0] == '/missing':
for location in [k for k, v in Regions.lookup_name_to_id.items() for location in [k for k, v in ctx.lookup_name_to_id.items()
if type(v) is int and not filter_location(ctx, k)]: if type(v) is int and not filter_location(ctx, k)]:
if location not in ctx.locations_checked: if location not in ctx.locations_checked:
logging.info('Missing: ' + location) logging.info('Missing: ' + location)
@@ -804,15 +818,19 @@ def get_item_name_from_id(code):
return items[0] if items else f'Unknown item (ID:{code})' return items[0] if items else f'Unknown item (ID:{code})'
def get_location_name_from_address(address): def get_location_name_from_address(ctx, address):
if type(address) is str: if type(address) is str:
return address return address
return Regions.lookup_id_to_name.get(address, f'Unknown location (ID:{address})') return ctx.lookup_id_to_name.get(address, f'Unknown location (ID:{address})')
def filter_location(ctx, location): def filter_location(ctx, location):
if not ctx.key_drop_mode and ('Key Drop' in location or 'Pot Key' in location): if (not ctx.key_drop_mode and location in PotShuffle.key_drop_data
and PotShuffle.key_drop_data[location][0] == 'Drop'):
return True
if (not ctx.pottery_mode and location in PotShuffle.key_drop_data
and PotShuffle.key_drop_data[location][0] == 'Pot'):
return True return True
if not ctx.shop_mode and location in Regions.flat_normal_shops: if not ctx.shop_mode and location in Regions.flat_normal_shops:
return True return True
@@ -821,6 +839,31 @@ def filter_location(ctx, location):
return False return False
def init_lookups(ctx):
ctx.lookup_id_to_name = {x: y for x, y in Regions.lookup_id_to_name.items()}
ctx.lookup_name_to_id = {x: y for x, y in Regions.lookup_name_to_id.items()}
for location, datum in PotShuffle.key_drop_data.items():
type = datum[0]
if type == 'Drop':
location_id, super_tile, sprite_index = datum[1]
location_table_sprite_items[location] = (2 * super_tile, 0x8000 >> sprite_index)
ctx.lookup_name_to_id[location] = location_id
ctx.lookup_id_to_name[location_id] = location
for super_tile, pot_list in PotShuffle.vanilla_pots.items():
for pot_index, pot in enumerate(pot_list):
if pot.item != PotItem.Hole:
if pot.item == PotItem.Key:
loc_name = next(loc for loc, datum in PotShuffle.key_drop_data.items()
if datum[1] == super_tile)
else:
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
loc_name = f'{pot.room} {descriptor}'
location_table_pot_items[loc_name] = (2 * super_tile, 0x8000 >> pot_index)
location_id = Regions.pot_address(pot_index, super_tile)
ctx.lookup_name_to_id[loc_name] = location_id
ctx.lookup_id_to_name[location_id] = loc_name
async def track_locations(ctx : Context, roomid, roomdata): async def track_locations(ctx : Context, roomid, roomdata):
new_locations = [] new_locations = []
@@ -832,9 +875,12 @@ async def track_locations(ctx : Context, roomid, roomdata):
if ctx.mode_flags is None: if ctx.mode_flags is None:
flags = await snes_read(ctx, MODE_FLAGS, 1) flags = await snes_read(ctx, MODE_FLAGS, 1)
ctx.mode_flags = flags
ctx.key_drop_mode = flags[0] & 0x1 ctx.key_drop_mode = flags[0] & 0x1
ctx.shop_mode = flags[0] & 0x2 ctx.shop_mode = flags[0] & 0x2
ctx.retro_mode = flags[0] & 0x4 ctx.retro_mode = flags[0] & 0x4
ctx.pottery_mode = flags[0] & 0x8
ctx.mystery_mode = flags[0] & 0x10
def new_check(location): def new_check(location):
ctx.locations_checked.add(location) ctx.locations_checked.add(location)
@@ -842,12 +888,13 @@ async def track_locations(ctx : Context, roomid, roomdata):
if ignored: if ignored:
ctx.ignore_count += 1 ctx.ignore_count += 1
else: else:
logging.info(f"New check: {location} ({len(ctx.locations_checked)-ctx.ignore_count}/{ctx.total_locations})") total = '???' if ctx.mystery_mode else ctx.total_locations
new_locations.append(Regions.lookup_name_to_id[location]) logging.info(f"New check: {location} ({len(ctx.locations_checked)-ctx.ignore_count}/{total})")
new_locations.append(ctx.lookup_name_to_id[location])
try: try:
if ctx.shop_mode or ctx.retro_mode: if ctx.shop_mode or ctx.retro_mode:
misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN) misc_data = await snes_read(ctx, SHOP_SRAM_START, SHOP_SRAM_LEN)
for cnt, b in enumerate(misc_data): for cnt, b in enumerate(misc_data):
my_check = Regions.shop_table_by_location_id[0x400000 + cnt] my_check = Regions.shop_table_by_location_id[0x400000 + cnt]
if int(b) > 0 and my_check not in ctx.locations_checked: if int(b) > 0 and my_check not in ctx.locations_checked:
@@ -908,6 +955,22 @@ async def track_locations(ctx : Context, roomid, roomdata):
if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked: if misc_data[offset - 0x3c6] & mask != 0 and location not in ctx.locations_checked:
new_check(location) new_check(location)
if not all([location in ctx.locations_checked for location in location_table_pot_items.keys()]):
pot_items_data = await snes_read(ctx, POT_ITEMS_SRAM_START, ITEM_SRAM_SIZE)
if pot_items_data is not None:
for location, (offset, mask) in location_table_pot_items.items():
pot_value = pot_items_data[offset] | (pot_items_data[offset + 1] << 8)
if pot_value & mask != 0 and location not in ctx.locations_checked:
new_check(location)
if not all([location in ctx.locations_checked for location in location_table_sprite_items.keys()]):
sprite_items_data = await snes_read(ctx, SPRITE_ITEMS_SRAM_START, ITEM_SRAM_SIZE)
if sprite_items_data is not None:
for location, (offset, mask) in location_table_sprite_items.items():
sprite_value = sprite_items_data[offset] | (sprite_items_data[offset + 1] << 8)
if sprite_value & mask != 0 and location not in ctx.locations_checked:
new_check(location)
await send_msgs(ctx.socket, [['LocationChecks', new_locations]]) await send_msgs(ctx.socket, [['LocationChecks', new_locations]])
async def game_watcher(ctx : Context): async def game_watcher(ctx : Context):
@@ -955,7 +1018,7 @@ async def game_watcher(ctx : Context):
item = ctx.items_received[recv_index] item = ctx.items_received[recv_index]
logging.info('Received %s from %s (%s) (%d/%d in list)' % ( logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received))) get_location_name_from_address(ctx, item.location), recv_index + 1, len(ctx.items_received)))
recv_index += 1 recv_index += 1
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item])) snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
@@ -969,7 +1032,7 @@ async def game_watcher(ctx : Context):
if scout_location > 0 and scout_location not in ctx.locations_scouted: if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location) ctx.locations_scouted.add(scout_location)
logging.info(f'Scouting item at {list(Regions.lookup_id_to_name.keys())[scout_location - 1]}') logging.info(f'Scouting item at {list(ctx.lookup_id_to_name.keys())[scout_location - 1]}')
await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]]) await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]])
await track_locations(ctx, roomid, roomdata) await track_locations(ctx, roomid, roomdata)
@@ -984,6 +1047,7 @@ async def main():
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
ctx = Context(args.snes, args.connect, args.password) ctx = Context(args.snes, args.connect, args.password)
init_lookups(ctx)
input_task = asyncio.create_task(console_loop(ctx)) input_task = asyncio.create_task(console_loop(ctx))
+38 -8
View File
@@ -6,12 +6,15 @@ import json
import logging import logging
import re import re
import shlex import shlex
import ssl
import urllib.request import urllib.request
import websockets import websockets
import zlib import zlib
from BaseClasses import PotItem, PotFlags
import Items import Items
import Regions import Regions
import PotShuffle
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
class Client: class Client:
@@ -40,6 +43,9 @@ class Context:
self.clients = [] self.clients = []
self.received_items = {} self.received_items = {}
self.lookup_name_to_id = {}
self.lookup_id_to_name = {}
async def send_msgs(websocket, msgs): async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed: if not websocket or not websocket.open or websocket.closed:
return return
@@ -154,8 +160,7 @@ def send_new_items(ctx : Context):
client.send_index = len(items) client.send_index = len(items)
def forfeit_player(ctx : Context, team, slot): def forfeit_player(ctx : Context, team, slot):
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int} all_locations = set(ctx.lookup_id_to_name.keys())
all_locations.update({values[1] for values in Regions.key_drop_data.values()})
notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1)) notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations) register_location_checks(ctx, team, slot, all_locations)
@@ -176,7 +181,8 @@ def register_location_checks(ctx : Context, team, slot, locations):
recvd_items.append(new_item) recvd_items.append(new_item)
if slot != target_player: if slot != target_player:
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]]) broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location))) loc_name = get_location_name_from_address(ctx, location)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], loc_name))
found_items = True found_items = True
send_new_items(ctx) send_new_items(ctx)
@@ -249,11 +255,11 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
return return
locs = [] locs = []
for location in args: for location in args:
if type(location) is not int or 0 >= location > len(Regions.lookup_id_to_name.keys()): if type(location) is not int or 0 >= location > len(ctx.lookup_id_to_name.keys()):
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']]) await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
return return
loc_name = list(Regions.lookup_id_to_name.keys())[location - 1] loc_name = list(ctx.lookup_id_to_name.keys())[location - 1]
target_item, target_player = ctx.locations[(Regions.lookup_name_to_id[loc_name], client.slot)] target_item, target_player = ctx.locations[(ctx.lookup_name_to_id[loc_name], client.slot)]
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D} replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item] item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
@@ -339,6 +345,30 @@ async def console(ctx : Context):
if command[0][0] != '/': if command[0][0] != '/':
notify_all(ctx, '[Server]: ' + input) notify_all(ctx, '[Server]: ' + input)
def init_lookups(ctx):
ctx.lookup_id_to_name = {x: y for x, y in Regions.lookup_id_to_name.items()}
ctx.lookup_name_to_id = {x: y for x, y in Regions.lookup_name_to_id.items()}
for location, datum in PotShuffle.key_drop_data.items():
type = datum[0]
if type == 'Drop':
location_id = datum[1][0]
ctx.lookup_name_to_id[location] = location_id
ctx.lookup_id_to_name[location_id] = location
for super_tile, pot_list in PotShuffle.vanilla_pots.items():
for pot_index, pot in enumerate(pot_list):
if pot.item != PotItem.Hole:
if pot.item == PotItem.Key:
loc_name = next(loc for loc, datum in PotShuffle.key_drop_data.items()
if datum[1] == super_tile)
else:
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
loc_name = f'{pot.room} {descriptor}'
location_id = Regions.pot_address(pot_index, super_tile)
ctx.lookup_name_to_id[loc_name] = location_id
ctx.lookup_id_to_name[location_id] = loc_name
async def main(): async def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--host', default=None) parser.add_argument('--host', default=None)
@@ -353,7 +383,7 @@ async def main():
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
ctx = Context(args.host, args.port, args.password) ctx = Context(args.host, args.port, args.password)
init_lookups(ctx)
ctx.data_filename = args.multidata ctx.data_filename = args.multidata
try: try:
@@ -376,7 +406,7 @@ async def main():
logging.error('Failed to read multiworld data (%s)' % e) logging.error('Failed to read multiworld data (%s)' % e)
return return
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host ip = urllib.request.urlopen('https://v4.ident.me', context=ssl._create_unverified_context()).read().decode('utf8') if not ctx.host else ctx.host
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password)) logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
ctx.disable_save = args.disable_save ctx.disable_save = args.disable_save
+13 -5
View File
@@ -30,6 +30,7 @@ def main():
parser.add_argument('--create_spoiler', action='store_true') parser.add_argument('--create_spoiler', action='store_true')
parser.add_argument('--no_race', action='store_true') parser.add_argument('--no_race', action='store_true')
parser.add_argument('--suppress_rom', action='store_true') parser.add_argument('--suppress_rom', action='store_true')
parser.add_argument('--suppress_meta', action='store_true')
parser.add_argument('--bps', action='store_true') parser.add_argument('--bps', action='store_true')
parser.add_argument('--rom') parser.add_argument('--rom')
parser.add_argument('--enemizercli') parser.add_argument('--enemizercli')
@@ -65,12 +66,14 @@ def main():
erargs.names = args.names erargs.names = args.names
erargs.create_spoiler = args.create_spoiler erargs.create_spoiler = args.create_spoiler
erargs.suppress_rom = args.suppress_rom erargs.suppress_rom = args.suppress_rom
erargs.suppress_meta = args.suppress_meta
erargs.bps = args.bps erargs.bps = args.bps
erargs.race = not args.no_race erargs.race = not args.no_race
erargs.outputname = seedname erargs.outputname = seedname
if args.outputpath: if args.outputpath:
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.loglevel = args.loglevel erargs.loglevel = args.loglevel
erargs.mystery = True
if args.rom: if args.rom:
erargs.rom = args.rom erargs.rom = args.rom
@@ -142,8 +145,8 @@ def roll_settings(weights):
ret.algorithm = get_choice('algorithm') ret.algorithm = get_choice('algorithm')
glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owg': 'owglitches', glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches',
'minorglitches': 'minorglitches'} 'owg': 'owglitches', 'minorglitches': 'minorglitches'}
glitches_required = get_choice('glitches_required') glitches_required = get_choice('glitches_required')
if glitches_required is not None: if glitches_required is not None:
if glitches_required not in glitch_map.keys(): if glitches_required not in glitch_map.keys():
@@ -179,6 +182,7 @@ def roll_settings(weights):
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.intensity = get_choice('intensity') ret.intensity = get_choice('intensity')
ret.experimental = get_choice('experimental') == 'on' ret.experimental = get_choice('experimental') == 'on'
ret.collection_rate = get_choice('collection_rate') == 'on'
ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default' ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default'
if ret.dungeon_counters == 'default': if ret.dungeon_counters == 'default':
@@ -186,7 +190,10 @@ def roll_settings(weights):
ret.pseudoboots = get_choice('pseudoboots') == 'on' ret.pseudoboots = get_choice('pseudoboots') == 'on'
ret.shopsanity = get_choice('shopsanity') == 'on' ret.shopsanity = get_choice('shopsanity') == 'on'
ret.keydropshuffle = get_choice('keydropshuffle') == 'on' ret.dropshuffle = get_choice('dropshuffle') == 'on'
ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none'
ret.colorizepots = get_choice('colorizepots') == 'on'
ret.shufflepots = get_choice('pot_shuffle') == 'on'
ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent' ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent'
ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize' ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize'
@@ -262,8 +269,6 @@ def roll_settings(weights):
ret.enemy_health = get_choice('enemy_health') ret.enemy_health = get_choice('enemy_health')
ret.shufflepots = get_choice('pot_shuffle') == 'on'
ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0' ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0'
inventoryweights = weights.get('startinventory', {}) inventoryweights = weights.get('startinventory', {})
@@ -272,6 +277,8 @@ def roll_settings(weights):
if get_choice(item, inventoryweights) == 'on': if get_choice(item, inventoryweights) == 'on':
startitems.append(item) startitems.append(item)
ret.startinventory = ','.join(startitems) ret.startinventory = ','.join(startitems)
if len(startitems) > 0:
ret.usestartinventory = True
if 'rom' in weights: if 'rom' in weights:
romweights = weights['rom'] romweights = weights['rom']
@@ -279,6 +286,7 @@ def roll_settings(weights):
ret.disablemusic = get_choice('disablemusic', romweights) == 'on' ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
ret.quickswap = get_choice('quickswap', romweights) == 'on' ret.quickswap = get_choice('quickswap', romweights) == 'on'
ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on' ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on'
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
ret.fastmenu = get_choice('menuspeed', romweights) ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights) ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = get_choice('heartbeep', romweights) ret.heartbeep = get_choice('heartbeep', romweights)
+2 -2
View File
@@ -727,7 +727,7 @@ OWTileRegions = bidict({
'Zora Waterfall Area': 0x0f, 'Zora Waterfall Area': 0x0f,
'Zora Waterfall Water': 0x0f, 'Zora Waterfall Water': 0x0f,
'Waterfall of Wishing Cave': 0x0f, 'Zora Waterfall Entryway': 0x0f,
'Lost Woods Pass West Area': 0x10, 'Lost Woods Pass West Area': 0x10,
'Lost Woods Pass East Top Area': 0x10, 'Lost Woods Pass East Top Area': 0x10,
@@ -1253,7 +1253,7 @@ OWExitTypes = {
'Mountain Entry Entrance Rock (West)', 'Mountain Entry Entrance Rock (West)',
'Mountain Entry Entrance Rock (East)', 'Mountain Entry Entrance Rock (East)',
'Zora Waterfall Water Entry', 'Zora Waterfall Water Entry',
'Waterfall of Wishing Cave Entry', 'Zora Waterfall Water Approach',
'Zora Waterfall Landing', 'Zora Waterfall Landing',
'Lost Woods Pass Hammer (North)', 'Lost Woods Pass Hammer (North)',
'Lost Woods Pass Hammer (South)', 'Lost Woods Pass Hammer (South)',
+6 -9
View File
@@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions
from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel
from Utils import bidict from Utils import bidict
version_number = '0.2.7.3' version_number = '0.2.8.0'
version_branch = '' version_branch = ''
__version__ = '%s%s' % (version_number, version_branch) __version__ = '%s%s' % (version_number, version_branch)
@@ -882,8 +882,7 @@ def build_sectors(world, player):
# perform accessibility check on duplicate world # perform accessibility check on duplicate world
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world) base_world = copy_world(world, True)
world.key_logic = {}
# build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances)
regions = list(OWTileRegions.copy().keys()) regions = list(OWTileRegions.copy().keys())
@@ -958,9 +957,8 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F
if build_copy_world: if build_copy_world:
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world) base_world = copy_world(world, True)
base_world.override_bomb_check = True base_world.override_bomb_check = True
world.key_logic = {}
else: else:
base_world = world base_world = world
@@ -974,7 +972,7 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F
return explored_regions return explored_regions
def validate_layout(world, player): def validate_layout(world, player):
if world.accessibility[player] == 'beatable': if world.accessibility[player] == 'none':
return True return True
entrance_connectors = { entrance_connectors = {
@@ -1048,8 +1046,7 @@ def validate_layout(world, player):
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world) base_world = copy_world(world, True)
world.key_logic = {}
explored_regions = list() explored_regions = list()
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]:
@@ -1152,7 +1149,7 @@ mandatory_connections = [# Intra-tile OW Connections
('Zora Waterfall Landing', 'Zora Waterfall Area'), ('Zora Waterfall Landing', 'Zora Waterfall Area'),
('Zora Waterfall Water Drop', 'Zora Waterfall Water'), #flippers ('Zora Waterfall Water Drop', 'Zora Waterfall Water'), #flippers
('Zora Waterfall Water Entry', 'Zora Waterfall Water'), #flippers ('Zora Waterfall Water Entry', 'Zora Waterfall Water'), #flippers
('Waterfall of Wishing Cave Entry', 'Waterfall of Wishing Cave'), #flippers ('Zora Waterfall Water Approach', 'Zora Waterfall Entryway'), #flippers
('Lost Woods Pass Hammer (North)', 'Lost Woods Pass Portal Area'), #hammer ('Lost Woods Pass Hammer (North)', 'Lost Woods Pass Portal Area'), #hammer
('Lost Woods Pass Hammer (South)', 'Lost Woods Pass East Top Area'), #hammer ('Lost Woods Pass Hammer (South)', 'Lost Woods Pass East Top Area'), #hammer
('Lost Woods Pass Rock (North)', 'Lost Woods Pass East Bottom Area'), #mitts ('Lost Woods Pass Rock (North)', 'Lost Woods Pass East Bottom Area'), #mitts
+1007 -267
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -140,7 +140,7 @@ New flute spots are chosen at random with minimum bias.
### Trinity ### Trinity
This goal gives you the choice between 3 goals, only one of which the player needs to complete: Defeat Ganon (no Aga2), Pulling Pedestal, or turn in TF pieces to Murahdulah. By default, you need to find 8 of 10 total TF pieces but this can be changed with a Custom Item Pool. It is recommended to set GT Entry to 7 crystals and Ganon to 5 crystals or Random crystals, although the player can flexibly change these settings as they seem fit. This goal gives you the choice between 3 goals, only one of which the player needs to complete: Defeat Ganon (no Aga2), Pulling Pedestal, or turn in TF pieces to Murahdahla. By default, you need to find 8 of 10 total TF pieces but this can be changed with a Custom Item Pool. It is recommended to set GT Entry to 7 crystals and Ganon to 5 crystals or Random crystals, although the player can flexibly change these settings as they seem fit.
## New Entrance Shuffle Options (--shuffle) ## New Entrance Shuffle Options (--shuffle)
+140 -18
View File
@@ -1,7 +1,77 @@
## New Features ## New Features
## Restricted Item Placement Algorithm ## Pottery Lottery and Key Drop Shuffle Changes
### Pottery
New pottery option that control which pots (and large blocks) are in the locations pool:
* None: No pots are in the pool, like normal randomizer
* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option
* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does
not include key pots.
* CaveKeys: Both non-dungeon pots and pots that used to have keys are in the pool.
* Reduced: Same as CaveKeys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents.
* Clustered: LIke reduced but pot are grouped by logical sets and roughly 50% of pots are chosen from those group. This is a dynamic mode like the above.
* Nonempty: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons.
* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks)
* Lottery: All pots and large blocks are in the pool
By default, switches remain in their vanilla location (unless you turn on the legacy option below)
CLI `--pottery <option>` from `none, keys, cave, cavekeys, reduced, clustered, nonempty, dungeon, lottery`
Note for multiworld: due to the design of the pottery lottery, only 256 items for other players can be under pots in your world.
### Colorize Pots
If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exceptions include "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. Note that Mystery will colorize all pots if lottery is chosen randomly.
CLI `--colorizepots`
### Shuffle key drops
Enemies that drop keys can have their drop shuffled into the pool. This is the other half of the keydropshuffle option.
CLI `--dropshuffle`
#### Legacy options
"Drop and Pot Keys" or `--keydropshuffle` is still availabe for use. This simply sets the pottery to keys and turns dropshuffle on as well to have the same behavior as the old setting.
The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or `--shufflepots` and works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode.
#### Tracking Notes
The sram locations for pots and sprite drops are now final, please reach out for assistance or investigate the rom changes if needed.
## New Options
### Collection Rate
You can set the collection rate counter on using the "Display Collection Rate" on the Game Options tab are using the CLI option `--collection_rate`. Mystery seeds will not display the total.
### Goal: Trinity
Triforces are placed behind Ganon, on the pedestal, and on Murahdahla with 8/10 triforce pieces required. Recommend to run with 4-5 Crystal requirement for Ganon. Automatically pre-opens the pyramid.
### Boss Shuffle: Unique
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
### MSU Resume
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
### BPS Patch
Creates a bps patch for the seed. Found on the "Generation Setup" tab called "Create BPS Patches" or `--bps`. Can turn off generating a rom using the existing "Create Patched ROM" option or `--suppress_rom`. There is an option on the Adjust/Patch tab to select a bps file to apply to the Base Rom selected on the Generation Setup tab using the Patch Rom button. Selected adjustments will be applied during patching.
## New Font
Font updated to support lowercase English. Lowercase vs. uppercase typos may exist. Note, you can use lowercase English letters on the file name.
## Restricted Item Placement Algorithm
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed. The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
@@ -13,10 +83,6 @@ The "Item Sorting" option or ```--algorithm``` has been updated with new placeme
This one stays the same as before and is recommended for the most random distribution of items. This one stays the same as before and is recommended for the most random distribution of items.
### Equitable
This one is currently under development and may not fill correctly. It is a new method that should allow item and key logic to interact. (Vanilla key placement in PoD is theoretically possible, but isn't yet.)
### Vanilla Fill ### Vanilla Fill
This fill attempts to place all items in their vanilla locations when possible. Obviously shuffling entrances or the dungeon interiors will often prevent items from being placed in their vanilla location. If the vanilla fill is not possible, then other locations are tried in sequence preferring "major" locations (see below), then heart piece locations, then the rest except for GT locations which are preferred last. Note the PoD small key that is normally found in the dark maze in vanilla is move to Harmless Hellway due to the placement algorithm limitation. This fill attempts to place all items in their vanilla locations when possible. Obviously shuffling entrances or the dungeon interiors will often prevent items from being placed in their vanilla location. If the vanilla fill is not possible, then other locations are tried in sequence preferring "major" locations (see below), then heart piece locations, then the rest except for GT locations which are preferred last. Note the PoD small key that is normally found in the dark maze in vanilla is move to Harmless Hellway due to the placement algorithm limitation.
@@ -33,12 +99,12 @@ The fill attempts to place all major items in dungeons. It will overflow to the
### District Restriction ### District Restriction
The world is divided up into different regions or districts. Each dungeon is it's own district. The overworld consists of the following districts: The world is divided up into different regions or districts. Each dungeon is its own district. The overworld consists of the following districts:
Light world: Light world:
* Kakariko (The main screen, blacksmith screen, and library/maze race screens) * Kakariko (The main screen, blacksmith screen, and library/maze race screens)
* Northwest Hyrule (The lost woods and fortune teller all the way to the rive west of the potion shop) * Northwest Hyrule (The lost woods and fortune teller screens all the way to the river west of the potion shop)
* Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove) * Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove)
* Desert (From the thief to the main desert screen) * Desert (From the thief to the main desert screen)
* Lake Hylia (Around the lake) * Lake Hylia (Around the lake)
@@ -63,14 +129,13 @@ In multiworld, the districts chosen apply to all players.
### CLI values: ### CLI values:
```balanced, equitable, vanilla_fill, major_only, dungeon_only, district``` ```balanced, vanilla_fill, major_only, dungeon_only, district```
## New Hints ## New Hints
Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it check for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish. At least two "foolish" districts are chosen and the rest are random. Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish.
## Overworld Map shows Dungeon Entrances
### Overworld Map shows dungeon location
Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower. Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower.
@@ -106,7 +171,9 @@ As before, the boss may have any item including any dungeon item that could occu
##### mapcompass ##### mapcompass
The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss. ~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~
Currently bugged, not recommended for use.
##### dungeon ##### dungeon
@@ -114,15 +181,70 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o
## Notes and Bug Fixes ## Notes and Bug Fixes
#### Unstable
* 1.0.1.0
* Large features
* New pottery modes - see notes above
* Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents.
* Updated TFH to support up to 850 pieces
* New font support
* Trinity goal added
* Separated Collection Rate counter from experimental
* Added MSU Resume option
* Support for BPS patch creation and applying patches during adjustment
* New option for Boss Shuffle: Unique (Prize bosses will be one of each, but GT bosses can be anything)
* Logic Notes
* Skull X Room requires Boots or access to Skull Back Drop
* GT Falling Torches requires Boots to get over the falling tile gap (this is a stop-gap measure until more sophisticated crystal switch traversal is possible)
* Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well)
* Fix for GT Crystal Conveyor not requiring Somaria/Bombs to get through
* Pedestal goal + vanilla swords places a random sword in the pool
* Added a few more places Links House shouldn't go when shuffled
* Small features
* Added a check for python package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea.
* Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work.
* Updated tourney winners (included Doors Async League winners)
* Some textual changes for hints (capitalization standardization)
* Item will be highlighted in red if experimental is on. This will likely be removed.
* Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill.
* Expanded Mystery logic options (e.g. owglitches)
* Updated indicators on keysanity menu for overworld map option
* Bug fixes:
* Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal.
* Bug with 0 GT crystals not opening GT
* Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking
* Settings code fix
* Fix for forbidding certain dashable doors (it actually does something this time)
* Fix for major item algorithm and pottery
* Updated map display on keysanity menu to work better with overworld_map option
* Minor bug in crossed doors
* Fix for Multiworld forfeits, shops and pot items now included
* MultiServer fix for ssl certs and python
* forbid certain doors from being dashable when you either can't dash them open (but bombs would work) or you'd fall into a pit from the bonk recoil in OHKO
* Fixed a couple rain state issues
* Add major_only algorithm to settings code
* Fixes for Links House being at certain entrances (did not generate)
* Fix for vanilla_fill, it now prioritizes heart container placements
* Fix for dungeon counter showing up in AT/HC in crossed dungeon mode
* Fixed usestartinventory with mystery
* Added double click fix for install.py
* Fix for SFX shuffle
* Fix for districting + shopsanity
* Fix for multiworld progression balancing would place Nothing or Arrow items
* Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count
* Fixed usestartinventory flag (can be use on a per player basis)
* Sprite selector fix for systems with SSL issues
* Fix for Standard ER where locations in rain state could be in logic
* 1.0.0.3 * 1.0.0.3
* overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown) * overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown)
* Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix) * Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix)
* Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks) * Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks)
* 1.0.0.2 * 1.0.0.2
* Include 1.0.1 fixes * Include 1.0.1 fixes
* District hint rework * District hint rework
* 1.0.0.1 * 1.0.0.1
* Add Light Hype Fairy to bombbag mode as needing bombs * Add Light Hype Fairy to bombbag mode as needing bombs
### From stable DoorDev ### From stable DoorDev
+130 -82
View File
@@ -1,6 +1,7 @@
import collections import collections
from Items import ItemFactory from Items import ItemFactory
from BaseClasses import Region, Location, Entrance, RegionType, Terrain, Shop, ShopType from BaseClasses import Region, Location, Entrance, RegionType, Terrain, Shop, ShopType, LocationType, PotItem, PotFlags
from PotShuffle import key_drop_data, vanilla_pots, choose_pots, PotSecretTable
def create_regions(world, player): def create_regions(world, player):
@@ -31,8 +32,8 @@ 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 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'], 'a ledge in the foothills'), create_lw_region(player, 'Mountain Entry Ledge', None, ['Mountain Entry Ledge Drop', 'Death Mountain Return Cave (West)', 'Bumper Cave Ledge Mirror Spot'], 'a ledge in the foothills'),
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 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'], 'Light World', Terrain.Water), create_lw_region(player, 'Zora Waterfall Water', None, ['Zora Waterfall Water Approach', 'Zora Waterfall Landing', 'Zora Whirlpool'], 'Light World', Terrain.Water),
create_lw_region(player, 'Waterfall of Wishing Cave', None, ['Zora Waterfall Water Drop', 'Waterfall of Wishing']), create_lw_region(player, 'Zora Waterfall Entryway', 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, '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']), create_lw_region(player, 'Lost Woods Pass West Area', None, ['Skull Woods Pass West Mirror Spot', 'Lost Woods Pass NW', 'Lost Woods Pass SW']),
create_lw_region(player, 'Lost Woods Pass East Top Area', None, ['Skull Woods Pass East Top Mirror Spot', 'Lost Woods Pass Hammer (North)', 'Lost Woods Pass NE']), create_lw_region(player, 'Lost Woods Pass East Top Area', None, ['Skull Woods Pass East Top Mirror Spot', 'Lost Woods Pass Hammer (North)', 'Lost Woods Pass NE']),
@@ -233,14 +234,16 @@ def create_regions(world, player):
create_cave_region(player, 'Old Man Cave Ledge', 'a connector', None, ['Old Man Cave Exit (West)', 'Old Man Cave Dropdown']), create_cave_region(player, 'Old Man Cave Ledge', 'a connector', None, ['Old Man Cave Exit (West)', 'Old Man Cave Dropdown']),
create_cave_region(player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']), create_cave_region(player, 'Old Man House', 'a connector', None, ['Old Man House Exit (Bottom)', 'Old Man House Front to Back']),
create_cave_region(player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']), create_cave_region(player, 'Old Man House Back', 'a connector', None, ['Old Man House Exit (Top)', 'Old Man House Back to Front']),
create_cave_region(player, 'Death Mountain Return Cave', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)']), create_cave_region(player, 'Death Mountain Return Cave (left)', 'a connector', None, ['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave E']),
create_cave_region(player, 'Death Mountain Return Cave (right)', 'a connector', None, ['Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave W']),
create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']), create_cave_region(player, 'Spectacle Rock Cave (Top)', 'a connector', ['Spectacle Rock Cave'], ['Spectacle Rock Cave Drop', 'Spectacle Rock Cave Exit (Top)']),
create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']), create_cave_region(player, 'Spectacle Rock Cave (Bottom)', 'a connector', None, ['Spectacle Rock Cave Exit']),
create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']), create_cave_region(player, 'Spectacle Rock Cave (Peak)', 'a connector', None, ['Spectacle Rock Cave Peak Drop', 'Spectacle Rock Cave Exit (Peak)']),
create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'), create_cave_region(player, 'Hookshot Fairy', 'fairies deep in a cave'),
create_cave_region(player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']), create_cave_region(player, 'Paradox Cave Front', 'a connector', None, ['Paradox Cave Push Block Reverse', 'Paradox Cave Exit (Bottom)', 'Light World Death Mountain Shop']),
create_cave_region(player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', create_cave_region(player, 'Paradox Cave Chest Area', 'a connector', ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle'],
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right'], ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump']), ['Paradox Cave Push Block', 'Paradox Cave Bomb Jump', 'Paradox Cave Chest Area NE']),
create_cave_region(player, 'Paradox Cave Bomb Area', 'a connector', ['Paradox Cave Upper - Left', 'Paradox Cave Upper - Right']),
create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']), create_cave_region(player, 'Paradox Cave', 'a connector', None, ['Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Paradox Cave Drop']),
create_cave_region(player, 'Light World Death Mountain Shop', 'a common shop', ['Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right']), create_cave_region(player, 'Light World Death Mountain Shop', 'a common shop', ['Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right']),
create_cave_region(player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']), create_cave_region(player, 'Fairy Ascension Cave (Bottom)', 'a connector', None, ['Fairy Ascension Cave Climb', 'Fairy Ascension Cave Exit (Bottom)']),
@@ -257,11 +260,13 @@ def create_regions(world, player):
create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']), create_cave_region(player, 'Kings Grave', 'a cave with a chest', ['King\'s Tomb']),
create_cave_region(player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']), create_cave_region(player, 'North Fairy Cave', 'a drop\'s exit', None, ['North Fairy Cave Exit']),
create_cave_region(player, 'Potion Shop', 'the potion shop', ['Potion Shop', 'Potion Shop - Left', 'Potion Shop - Middle', 'Potion Shop - Right']), create_cave_region(player, 'Potion Shop', 'the potion shop', ['Potion Shop', 'Potion Shop - Left', 'Potion Shop - Middle', 'Potion Shop - Right']),
create_cave_region(player, 'Kakariko Well (top)', 'a drop\'s exit', ['Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', create_cave_region(player, 'Kakariko Well (top)', 'a drop', ['Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right',
'Kakariko Well - Right', 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)']), 'Kakariko Well - Bottom'], ['Kakariko Well (top to bottom)', 'Kakariko Well (top to back)']),
create_cave_region(player, 'Kakariko Well (back)', 'a drop', ['Kakariko Well - Top']),
create_cave_region(player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']), create_cave_region(player, 'Kakariko Well (bottom)', 'a drop\'s exit', None, ['Kakariko Well Exit']),
create_cave_region(player, 'Blinds Hideout', 'a bounty of five items', ["Blind\'s Hideout - Top", "Blind\'s Hideout - Left", create_cave_region(player, 'Blinds Hideout', 'a bounty of five items', ["Blind's Hideout - Left", "Blind's Hideout - Right",
"Blind\'s Hideout - Right", "Blind\'s Hideout - Far Left", "Blind\'s Hideout - Far Right"]), "Blind's Hideout - Far Left", "Blind's Hideout - Far Right"], ['Blinds Hideout N']),
create_cave_region(player, 'Blinds Hideout (Top)', 'a bounty of five items', ["Blind's Hideout - Top"]),
create_cave_region(player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']), create_cave_region(player, 'Elder House', 'a connector', None, ['Elder House Exit (East)', 'Elder House Exit (West)']),
create_cave_region(player, 'Snitch Lady (West)', 'a boring house'), create_cave_region(player, 'Snitch Lady (West)', 'a boring house'),
create_cave_region(player, 'Snitch Lady (East)', 'a boring house'), create_cave_region(player, 'Snitch Lady (East)', 'a boring house'),
@@ -276,7 +281,7 @@ def create_regions(world, player):
create_cave_region(player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']), create_cave_region(player, 'Sahasrahlas Hut', 'Sahasrahla', ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla']),
create_cave_region(player, 'Blacksmiths Hut', 'the smith', ['Blacksmith'], ['Missing Smith']), create_cave_region(player, 'Blacksmiths Hut', 'the smith', ['Blacksmith'], ['Missing Smith']),
create_cave_region(player, 'Missing Smith', None, ['Missing Smith']), create_cave_region(player, 'Missing Smith', None, ['Missing Smith']),
create_cave_region(player, 'Bat Cave (right)', 'a drop\'s exit', ['Magic Bat'], ['Bat Cave Door']), create_cave_region(player, 'Bat Cave (right)', 'a drop', ['Magic Bat'], ['Bat Cave Door']),
create_cave_region(player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']), create_cave_region(player, 'Bat Cave (left)', 'a drop\'s exit', None, ['Bat Cave Exit']),
create_cave_region(player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']), create_cave_region(player, 'Two Brothers House', 'a connector', None, ['Two Brothers House Exit (East)', 'Two Brothers House Exit (West)']),
create_cave_region(player, 'Library', 'the library', ['Library']), create_cave_region(player, 'Library', 'the library', ['Library']),
@@ -305,8 +310,10 @@ def create_regions(world, player):
create_cave_region(player, 'Dark World Lumberjack Shop', 'a common shop', ['Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right']), create_cave_region(player, 'Dark World Lumberjack Shop', 'a common shop', ['Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right']),
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']), create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
create_cave_region(player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'), create_cave_region(player, 'Dark Death Mountain Healer Fairy', 'a fairy fountain'),
create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'], create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', None,
['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit']), ['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit', 'Hookshot Cave Bonk Path', 'Hookshot Cave Hook Path']),
create_cave_region(player, 'Hookshot Cave (Bonk Islands)', 'a connector', ['Hookshot Cave - Bottom Right']),
create_cave_region(player, 'Hookshot Cave (Hook Islands)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Left']),
create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']), create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']),
create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']), create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']),
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']), create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
@@ -502,7 +509,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Hera Startile Wide - Crystal', 'Tower of Hera', None, ['Hera Startile Wide Crystal Exit']), create_dungeon_region(player, 'Hera Startile Wide - Crystal', 'Tower of Hera', None, ['Hera Startile Wide Crystal Exit']),
create_dungeon_region(player, 'Hera 4F', 'Tower of Hera', ['Tower of Hera - Compass Chest'], ['Hera 4F Down Stairs', 'Hera 4F Up Stairs', 'Hera Big Chest Hook Path', 'Hera 4F Holes']), create_dungeon_region(player, 'Hera 4F', 'Tower of Hera', ['Tower of Hera - Compass Chest'], ['Hera 4F Down Stairs', 'Hera 4F Up Stairs', 'Hera Big Chest Hook Path', 'Hera 4F Holes']),
create_dungeon_region(player, 'Hera Big Chest Landing', 'Tower of Hera', ['Tower of Hera - Big Chest'], ['Hera Big Chest Landing Exit', 'Hera Big Chest Landing Holes']), create_dungeon_region(player, 'Hera Big Chest Landing', 'Tower of Hera', ['Tower of Hera - Big Chest'], ['Hera Big Chest Landing Exit', 'Hera Big Chest Landing Holes']),
create_dungeon_region(player, 'Hera 5F', 'Tower of Hera', None, ['Hera 5F Down Stairs', 'Hera 5F Up Stairs', 'Hera 5F Star Hole', 'Hera 5F Pothole Chain', 'Hera 5F Normal Holes']), create_dungeon_region(player, 'Hera 5F', 'Tower of Hera', None, ['Hera 5F Down Stairs', 'Hera 5F Up Stairs', 'Hera 5F Star Hole', 'Hera 5F Pothole Chain', 'Hera 5F Normal Holes', 'Hera 5F Orange Path']),
create_dungeon_region(player, 'Hera 5F Pot Block', 'Tower of Hera', None),
create_dungeon_region(player, 'Hera Fairies', 'Tower of Hera', None, ['Hera Fairies\' Warp']), create_dungeon_region(player, 'Hera Fairies', 'Tower of Hera', None, ['Hera Fairies\' Warp']),
create_dungeon_region(player, 'Hera Boss', 'Tower of Hera', ['Tower of Hera - Boss', 'Tower of Hera - Prize'], ['Hera Boss Down Stairs', 'Hera Boss Outer Hole', 'Hera Boss Inner Hole']), create_dungeon_region(player, 'Hera Boss', 'Tower of Hera', ['Tower of Hera - Boss', 'Tower of Hera - Prize'], ['Hera Boss Down Stairs', 'Hera Boss Outer Hole', 'Hera Boss Inner Hole']),
@@ -590,7 +598,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Swamp Trench 1 Key Ledge', 'Swamp Palace', None, ['Swamp Trench 1 Key Ledge Dry', 'Swamp Trench 1 Key Approach', 'Swamp Trench 1 Key Ledge Depart', 'Swamp Trench 1 Key Ledge NW']), create_dungeon_region(player, 'Swamp Trench 1 Key Ledge', 'Swamp Palace', None, ['Swamp Trench 1 Key Ledge Dry', 'Swamp Trench 1 Key Approach', 'Swamp Trench 1 Key Ledge Depart', 'Swamp Trench 1 Key Ledge NW']),
create_dungeon_region(player, 'Swamp Trench 1 Departure', 'Swamp Palace', None, ['Swamp Trench 1 Departure Dry', 'Swamp Trench 1 Departure Approach', 'Swamp Trench 1 Departure Key', 'Swamp Trench 1 Departure WS']), create_dungeon_region(player, 'Swamp Trench 1 Departure', 'Swamp Palace', None, ['Swamp Trench 1 Departure Dry', 'Swamp Trench 1 Departure Approach', 'Swamp Trench 1 Departure Key', 'Swamp Trench 1 Departure WS']),
create_dungeon_region(player, 'Swamp Hammer Switch', 'Swamp Palace', ['Trench 1 Switch'], ['Swamp Hammer Switch SW', 'Swamp Hammer Switch WN']), create_dungeon_region(player, 'Swamp Hammer Switch', 'Swamp Palace', ['Trench 1 Switch'], ['Swamp Hammer Switch SW', 'Swamp Hammer Switch WN']),
create_dungeon_region(player, 'Swamp Hub', 'Swamp Palace', ['Swamp Palace - Big Chest', 'Swamp Palace - Hookshot Pot Key'], ['Swamp Hub ES', 'Swamp Hub S', 'Swamp Hub WS', 'Swamp Hub WN', 'Swamp Hub Hook Path']), create_dungeon_region(player, 'Swamp Hub', 'Swamp Palace', ['Swamp Palace - Big Chest'], ['Swamp Hub ES', 'Swamp Hub S', 'Swamp Hub WS', 'Swamp Hub WN', 'Swamp Hub Hook Path', 'Swamp Hub Side Hook Path']),
create_dungeon_region(player, 'Swamp Hub Side Ledges', 'Swamp Palace', ['Swamp Palace - Hookshot Pot Key']),
create_dungeon_region(player, 'Swamp Hub Dead Ledge', 'Swamp Palace', None, ['Swamp Hub Dead Ledge EN']), create_dungeon_region(player, 'Swamp Hub Dead Ledge', 'Swamp Palace', None, ['Swamp Hub Dead Ledge EN']),
create_dungeon_region(player, 'Swamp Hub North Ledge', 'Swamp Palace', None, ['Swamp Hub North Ledge N', 'Swamp Hub North Ledge Drop Down']), create_dungeon_region(player, 'Swamp Hub North Ledge', 'Swamp Palace', None, ['Swamp Hub North Ledge N', 'Swamp Hub North Ledge Drop Down']),
create_dungeon_region(player, 'Swamp Donut Top', 'Swamp Palace', None, ['Swamp Donut Top N', 'Swamp Donut Top SE']), create_dungeon_region(player, 'Swamp Donut Top', 'Swamp Palace', None, ['Swamp Donut Top N', 'Swamp Donut Top SE']),
@@ -645,8 +654,9 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Skull Big Key', 'Skull Woods', ['Skull Woods - Big Key Chest'], ['Skull Big Key SW', 'Skull Big Key EN']), create_dungeon_region(player, 'Skull Big Key', 'Skull Woods', ['Skull Woods - Big Key Chest'], ['Skull Big Key SW', 'Skull Big Key EN']),
create_dungeon_region(player, 'Skull Lone Pot', 'Skull Woods', None, ['Skull Lone Pot WN']), create_dungeon_region(player, 'Skull Lone Pot', 'Skull Woods', None, ['Skull Lone Pot WN']),
create_dungeon_region(player, 'Skull Small Hall', 'Skull Woods', None, ['Skull Small Hall ES', 'Skull Small Hall WS']), create_dungeon_region(player, 'Skull Small Hall', 'Skull Woods', None, ['Skull Small Hall ES', 'Skull Small Hall WS']),
create_dungeon_region(player, 'Skull Back Drop', 'Skull Woods', None, ['Skull Back Drop Star Path']), create_dungeon_region(player, 'Skull Back Drop', 'Skull Woods', ['Skull Star Tile'], ['Skull Back Drop Star Path']),
create_dungeon_region(player, 'Skull 2 West Lobby', 'Skull Woods', ['Skull Woods - West Lobby Pot Key'], ['Skull 2 West Lobby ES', 'Skull 2 West Lobby NW', 'Skull 2 West Lobby S']), create_dungeon_region(player, 'Skull 2 West Lobby', 'Skull Woods', ['Skull Woods - West Lobby Pot Key'], ['Skull 2 West Lobby ES', 'Skull 2 West Lobby Pits', 'Skull 2 West Lobby S']),
create_dungeon_region(player, 'Skull 2 West Lobby Ledge', 'Skull Woods', None, ['Skull 2 West Lobby NW', 'Skull 2 West Lobby Ledge Pits']),
create_dungeon_region(player, 'Skull X Room', 'Skull Woods', None, ['Skull X Room SW']), create_dungeon_region(player, 'Skull X Room', 'Skull Woods', None, ['Skull X Room SW']),
create_dungeon_region(player, 'Skull 3 Lobby', 'Skull Woods', None, ['Skull 3 Lobby NW', 'Skull 3 Lobby EN', 'Skull 3 Lobby SW']), create_dungeon_region(player, 'Skull 3 Lobby', 'Skull Woods', None, ['Skull 3 Lobby NW', 'Skull 3 Lobby EN', 'Skull 3 Lobby SW']),
create_dungeon_region(player, 'Skull East Bridge', 'Skull Woods', None, ['Skull East Bridge WN', 'Skull East Bridge WS']), create_dungeon_region(player, 'Skull East Bridge', 'Skull Woods', None, ['Skull East Bridge WN', 'Skull East Bridge WS']),
@@ -677,7 +687,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Thieves Hellway S Crystal', 'Thieves\' Town', None, ['Thieves Hellway Crystal Orange Barrier', 'Thieves Hellway Crystal ES']), create_dungeon_region(player, 'Thieves Hellway S Crystal', 'Thieves\' Town', None, ['Thieves Hellway Crystal Orange Barrier', 'Thieves Hellway Crystal ES']),
create_dungeon_region(player, 'Thieves Triple Bypass', 'Thieves\' Town', None, ['Thieves Triple Bypass WN', 'Thieves Triple Bypass EN', 'Thieves Triple Bypass SE']), create_dungeon_region(player, 'Thieves Triple Bypass', 'Thieves\' Town', None, ['Thieves Triple Bypass WN', 'Thieves Triple Bypass EN', 'Thieves Triple Bypass SE']),
create_dungeon_region(player, 'Thieves Spike Switch', 'Thieves\' Town', ['Thieves\' Town - Spike Switch Pot Key'], ['Thieves Spike Switch SW', 'Thieves Spike Switch Up Stairs']), create_dungeon_region(player, 'Thieves Spike Switch', 'Thieves\' Town', ['Thieves\' Town - Spike Switch Pot Key'], ['Thieves Spike Switch SW', 'Thieves Spike Switch Up Stairs']),
create_dungeon_region(player, 'Thieves Attic', 'Thieves\' Town', None, ['Thieves Attic Down Stairs', 'Thieves Attic ES', 'Thieves Attic Orange Barrier']), create_dungeon_region(player, 'Thieves Attic', 'Thieves\' Town', None, ['Thieves Attic Down Stairs', 'Thieves Attic ES', 'Thieves Attic Orange Barrier', 'Thieves Attic Blue Barrier']),
create_dungeon_region(player, 'Thieves Attic Switch', 'Thieves\' Town', None, ['Thieves Attic Switch Blue Barrier']),
create_dungeon_region(player, 'Thieves Attic Hint', 'Thieves\' Town', None, ['Thieves Attic Hint Orange Barrier']), create_dungeon_region(player, 'Thieves Attic Hint', 'Thieves\' Town', None, ['Thieves Attic Hint Orange Barrier']),
create_dungeon_region(player, 'Thieves Cricket Hall Left', 'Thieves\' Town', None, ['Thieves Cricket Hall Left WS', 'Thieves Cricket Hall Left Edge']), create_dungeon_region(player, 'Thieves Cricket Hall Left', 'Thieves\' Town', None, ['Thieves Cricket Hall Left WS', 'Thieves Cricket Hall Left Edge']),
create_dungeon_region(player, 'Thieves Cricket Hall Right', 'Thieves\' Town', None, ['Thieves Cricket Hall Right Edge', 'Thieves Cricket Hall Right ES']), create_dungeon_region(player, 'Thieves Cricket Hall Right', 'Thieves\' Town', None, ['Thieves Cricket Hall Right Edge', 'Thieves Cricket Hall Right ES']),
@@ -805,7 +816,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'TR Main Lobby', 'Turtle Rock', None, ['TR Main Lobby Gap', 'TR Main Lobby SE']), create_dungeon_region(player, 'TR Main Lobby', 'Turtle Rock', None, ['TR Main Lobby Gap', 'TR Main Lobby SE']),
create_dungeon_region(player, 'TR Lobby Ledge', 'Turtle Rock', None, ['TR Lobby Ledge NE', 'TR Lobby Ledge Gap']), create_dungeon_region(player, 'TR Lobby Ledge', 'Turtle Rock', None, ['TR Lobby Ledge NE', 'TR Lobby Ledge Gap']),
create_dungeon_region(player, 'TR Compass Room', 'Turtle Rock', ['Turtle Rock - Compass Chest'], ['TR Compass Room NW']), create_dungeon_region(player, 'TR Compass Room', 'Turtle Rock', ['Turtle Rock - Compass Chest'], ['TR Compass Room NW']),
create_dungeon_region(player, 'TR Hub', 'Turtle Rock', None, ['TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE']), create_dungeon_region(player, 'TR Hub', 'Turtle Rock', None, ['TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path']),
create_dungeon_region(player, 'TR Hub Ledges', 'Turtle Rock', None, ['TR Hub Ledges Path']),
create_dungeon_region(player, 'TR Torches Ledge', 'Turtle Rock', None, ['TR Torches Ledge WS']), create_dungeon_region(player, 'TR Torches Ledge', 'Turtle Rock', None, ['TR Torches Ledge WS']),
create_dungeon_region(player, 'TR Torches', 'Turtle Rock', None, ['TR Torches WN', 'TR Torches NW']), create_dungeon_region(player, 'TR Torches', 'Turtle Rock', None, ['TR Torches WN', 'TR Torches NW']),
create_dungeon_region(player, 'TR Roller Room', 'Turtle Rock', ['Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right'], ['TR Roller Room SW']), create_dungeon_region(player, 'TR Roller Room', 'Turtle Rock', ['Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right'], ['TR Roller Room SW']),
@@ -842,7 +854,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'TR Crystaroller Chest', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['TR Crystaroller Chest to Middle Barrier - Blue']), create_dungeon_region(player, 'TR Crystaroller Chest', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['TR Crystaroller Chest to Middle Barrier - Blue']),
create_dungeon_region(player, 'TR Crystaroller Middle - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Middle Ranged Crystal Exit']), create_dungeon_region(player, 'TR Crystaroller Middle - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Middle Ranged Crystal Exit']),
create_dungeon_region(player, 'TR Crystaroller Bottom - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Bottom Ranged Crystal Exit']), create_dungeon_region(player, 'TR Crystaroller Bottom - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Bottom Ranged Crystal Exit']),
create_dungeon_region(player, 'TR Dark Ride', 'Turtle Rock', None, ['TR Dark Ride Up Stairs', 'TR Dark Ride SW']), create_dungeon_region(player, 'TR Dark Ride', 'Turtle Rock', None, ['TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Dark Ride Path']),
create_dungeon_region(player, 'TR Dark Ride Ledges', 'Turtle Rock', None, ['TR Dark Ride Ledges Path']),
create_dungeon_region(player, 'TR Dash Bridge', 'Turtle Rock', None, ['TR Dash Bridge NW', 'TR Dash Bridge SW', 'TR Dash Bridge WS']), create_dungeon_region(player, 'TR Dash Bridge', 'Turtle Rock', None, ['TR Dash Bridge NW', 'TR Dash Bridge SW', 'TR Dash Bridge WS']),
create_dungeon_region(player, 'TR Eye Bridge', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', create_dungeon_region(player, 'TR Eye Bridge', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
@@ -852,7 +865,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'TR Crystal Maze Interior', 'Turtle Rock', None, ['TR Crystal Maze Interior to End Barrier - Blue', 'TR Crystal Maze Interior to Start Barrier - Blue', 'TR Crystal Maze Interior to Start Bypass', 'TR Crystal Maze Interior to End Bypass']), create_dungeon_region(player, 'TR Crystal Maze Interior', 'Turtle Rock', None, ['TR Crystal Maze Interior to End Barrier - Blue', 'TR Crystal Maze Interior to Start Barrier - Blue', 'TR Crystal Maze Interior to Start Bypass', 'TR Crystal Maze Interior to End Bypass']),
create_dungeon_region(player, 'TR Crystal Maze End', 'Turtle Rock', None, ['TR Crystal Maze North Stairs', 'TR Crystal Maze End to Interior Barrier - Blue', 'TR Crystal Maze End to Ranged Crystal']), create_dungeon_region(player, 'TR Crystal Maze End', 'Turtle Rock', None, ['TR Crystal Maze North Stairs', 'TR Crystal Maze End to Interior Barrier - Blue', 'TR Crystal Maze End to Ranged Crystal']),
create_dungeon_region(player, 'TR Crystal Maze End - Ranged Crystal', 'Turtle Rock', None, ['TR Crystal Maze End Ranged Crystal Exit']), create_dungeon_region(player, 'TR Crystal Maze End - Ranged Crystal', 'Turtle Rock', None, ['TR Crystal Maze End Ranged Crystal Exit']),
create_dungeon_region(player, 'TR Final Abyss', 'Turtle Rock', None, ['TR Final Abyss South Stairs', 'TR Final Abyss NW']), create_dungeon_region(player, 'TR Final Abyss Balcony', 'Turtle Rock', None, ['TR Final Abyss South Stairs', 'TR Final Abyss Balcony Path']),
create_dungeon_region(player, 'TR Final Abyss Ledge', 'Turtle Rock', None, ['TR Final Abyss NW', 'TR Final Abyss Ledge Path']),
create_dungeon_region(player, 'TR Boss', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize'], ['TR Boss SW']), create_dungeon_region(player, 'TR Boss', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize'], ['TR Boss SW']),
# gt # gt
@@ -877,9 +891,10 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'GT Invisible Bridges', 'Ganon\'s Tower', None, ['GT Invisible Bridges WS']), create_dungeon_region(player, 'GT Invisible Bridges', 'Ganon\'s Tower', None, ['GT Invisible Bridges WS']),
create_dungeon_region(player, 'GT Invisible Catwalk', 'Ganon\'s Tower', None, ['GT Invisible Catwalk ES', 'GT Invisible Catwalk WS', 'GT Invisible Catwalk NW', 'GT Invisible Catwalk NE']), create_dungeon_region(player, 'GT Invisible Catwalk', 'Ganon\'s Tower', None, ['GT Invisible Catwalk ES', 'GT Invisible Catwalk WS', 'GT Invisible Catwalk NW', 'GT Invisible Catwalk NE']),
create_dungeon_region(player, 'GT Conveyor Cross', 'Ganon\'s Tower', ['Ganons Tower - Conveyor Cross Pot Key'], ['GT Conveyor Cross EN', 'GT Conveyor Cross WN']), create_dungeon_region(player, 'GT Conveyor Cross', 'Ganon\'s Tower', ['Ganons Tower - Conveyor Cross Pot Key'], ['GT Conveyor Cross EN', 'GT Conveyor Cross WN']),
create_dungeon_region(player, 'GT Hookshot East Platform', 'Ganon\'s Tower', None, ['GT Hookshot EN', 'GT Hookshot East-North Path', 'GT Hookshot East-South Path']), create_dungeon_region(player, 'GT Hookshot East Platform', 'Ganon\'s Tower', None, ['GT Hookshot EN', 'GT Hookshot East-Mid Path']),
create_dungeon_region(player, 'GT Hookshot North Platform', 'Ganon\'s Tower', None, ['GT Hookshot NW', 'GT Hookshot North-East Path', 'GT Hookshot North-South Path']), create_dungeon_region(player, 'GT Hookshot Mid Platform', 'Ganon\'s Tower', None, ['GT Hookshot Mid-East Path', 'GT Hookshot Mid-South Path', 'GT Hookshot Mid-North Path']),
create_dungeon_region(player, 'GT Hookshot South Platform', 'Ganon\'s Tower', None, ['GT Hookshot ES', 'GT Hookshot South-East Path', 'GT Hookshot South-North Path', 'GT Hookshot Platform Blue Barrier', 'GT Hookshot Platform Barrier Bypass']), create_dungeon_region(player, 'GT Hookshot North Platform', 'Ganon\'s Tower', None, ['GT Hookshot NW', 'GT Hookshot North-Mid Path']),
create_dungeon_region(player, 'GT Hookshot South Platform', 'Ganon\'s Tower', None, ['GT Hookshot ES', 'GT Hookshot South-Mid Path', 'GT Hookshot Platform Blue Barrier', 'GT Hookshot Platform Barrier Bypass']),
create_dungeon_region(player, 'GT Hookshot South Entry', 'Ganon\'s Tower', None, ['GT Hookshot SW', 'GT Hookshot Entry Blue Barrier', 'GT Hookshot South Entry to Ranged Crystal']), create_dungeon_region(player, 'GT Hookshot South Entry', 'Ganon\'s Tower', None, ['GT Hookshot SW', 'GT Hookshot Entry Blue Barrier', 'GT Hookshot South Entry to Ranged Crystal']),
create_dungeon_region(player, 'GT Hookshot South Entry - Ranged Crystal', 'Ganon\'s Tower', None, ['GT HookShot South Entry Ranged Crystal Exit']), create_dungeon_region(player, 'GT Hookshot South Entry - Ranged Crystal', 'Ganon\'s Tower', None, ['GT HookShot South Entry Ranged Crystal Exit']),
create_dungeon_region(player, 'GT Map Room', 'Ganon\'s Tower', ['Ganons Tower - Map Chest'], ['GT Map Room WS']), create_dungeon_region(player, 'GT Map Room', 'Ganon\'s Tower', ['Ganons Tower - Map Chest'], ['GT Map Room WS']),
@@ -1145,42 +1160,111 @@ def create_shops(world, player):
def adjust_locations(world, player): def adjust_locations(world, player):
if world.keydropshuffle[player]: # handle pots
for location in key_drop_data.keys(): world.pot_contents[player] = PotSecretTable()
loc = world.get_location(location, player) for location, datum in key_drop_data.items():
loc = world.get_location(location, player)
drop_location = 'Drop' == datum[0]
if drop_location:
loc.type = LocationType.Drop
snes_address, room, sprite_idx = datum[1]
loc.address = snes_address
else:
loc.type = LocationType.Pot
pot, pot_index = next((p, i) for i, p in enumerate(vanilla_pots[datum[1]]) if p.item == PotItem.Key)
pot = pot.copy()
loc.address = pot_address(pot_index, datum[1])
loc.pot = pot
pot.location = loc
if (not world.dropshuffle[player] and drop_location)\
or (not drop_location and world.pottery[player] in ['none', 'cave']):
loc.skip = True
else:
key_item = loc.item key_item = loc.item
key_item.location = None key_item.location = None
loc.forced_item = None loc.forced_item = None
loc.item = None loc.item = None
loc.event = False loc.event = False
loc.address = key_drop_data[location][1] item_dungeon = key_item.dungeon
loc.player_address = key_drop_data[location][0]
item_dungeon = key_item.name.split('(')[1][:-1]
item_dungeon = 'Hyrule Castle' if item_dungeon == 'Escape' else item_dungeon
dungeon = world.get_dungeon(item_dungeon, player) dungeon = world.get_dungeon(item_dungeon, player)
if key_item.smallkey and not world.retro[player]: if key_item.smallkey and not world.retro[player]:
dungeon.small_keys.append(key_item) dungeon.small_keys.append(key_item)
elif key_item.bigkey: elif key_item.bigkey:
dungeon.big_key = key_item dungeon.big_key = key_item
world.pot_pool[player] = choose_pots(world, player)
for super_tile, pot_list in vanilla_pots.items():
for pot_index, pot_orig in enumerate(pot_list):
if pot_orig.item == PotItem.Key:
loc = next(location for location, datum in key_drop_data.items() if datum[1] == super_tile)
pot = world.get_location(loc, player).pot
else:
pot = pot_orig.copy()
world.pot_contents[player].room_map[super_tile].append(pot)
if valid_pot_location(pot, world.pot_pool[player], world, player):
create_pot_location(pot, pot_index, super_tile, world, player)
if world.shopsanity[player]: if world.shopsanity[player]:
index = 0 index = 0
for shop, location_list in shop_to_location_table.items(): for shop, location_list in shop_to_location_table.items():
for location in location_list: for location in location_list:
world.get_location(location, player).address = 0x400000 + index loc = world.get_location(location, player)
loc.address = 0x400000 + index
loc.type = LocationType.Shop
# player address? it is in the shop table # player address? it is in the shop table
index += 1 index += 1
# unreal events: # unreal events:
for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Dark Blacksmith Ruins', 'Middle Aged Man', for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Dark Blacksmith Ruins', 'Middle Aged Man',
'Frog', 'Missing Smith', 'Floodgate', 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Frog', 'Missing Smith', 'Floodgate', 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain',
'Attic Cracked Floor', 'Suspicious Maiden', 'Revealing Light', 'Big Bomb', 'Pyramid Crack', 'Attic Cracked Floor', 'Suspicious Maiden', 'Revealing Light', 'Big Bomb', 'Pyramid Crack',
'Ice Block Drop', 'Zelda Pickup', 'Zelda Drop Off']: 'Ice Block Drop', 'Zelda Pickup', 'Zelda Drop Off', 'Skull Star Tile']:
location = world.get_location_unsafe(l, player) location = world.get_location_unsafe(l, player)
if location: if location:
location.type = LocationType.Logical
location.real = False location.real = False
if l not in ['Ganon', 'Agahnim 1', 'Agahnim 2']:
location.skip = True
def valid_pot_location(pot, pot_set, world, player):
if world.pottery[player] == 'lottery':
return True
if world.pottery[player] == 'nonempty' and pot.item != PotItem.Nothing:
return True
if world.pottery[player] in ['reduced', 'clustered'] and pot in pot_set:
return True
if world.pottery[player] == 'dungeon' and world.get_region(pot.room, player).type == RegionType.Dungeon:
return True
if world.pottery[player] in ['cave', 'cavekeys'] and world.get_region(pot.room, player).type == RegionType.Cave:
return True
return False
def create_pot_location(pot, pot_index, super_tile, world, player):
if (pot.item not in [PotItem.Key, PotItem.Hole]
and (pot.item != PotItem.Switch or (world.potshuffle[player]
and world.pottery[player] not in ['none', 'cave', 'keys', 'cavekeys']))):
address = pot_address(pot_index, super_tile)
region = pot.room
parent = world.get_region(region, player)
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
hint_text = ('under a block' if pot.flags & PotFlags.Block else 'in a pot')
modifier = parent.hint_text not in {'a storyteller', 'fairies deep in a cave', 'a spiky hint',
'a bounty of five items', 'the sick kid', 'Sahasrahla'}
hint_text = f'{hint_text} {"in" if modifier else "near"} {parent.hint_text}'
pot_location = Location(player, f'{pot.room} {descriptor}', address, hint_text=hint_text,
parent=parent)
world.dynamic_locations.append(pot_location)
pot_location.pot = pot
pot.location = pot_location
pot_location.type = LocationType.Pot
parent.locations.append(pot_location)
def pot_address(pot_index, super_tile):
return 0x7f6018 + super_tile * 2 + (pot_index << 24)
# (type, room_id, shopkeeper, custom, locked, [items]) # (type, room_id, shopkeeper, custom, locked, [items])
# item = (item, price, max=0, replacement=None, replacement_price=0) # item = (item, price, max=0, replacement=None, replacement_price=0)
@@ -1232,42 +1316,6 @@ shop_table_by_location_id = {0x400000+cnt: x for cnt, x in enumerate(flat_normal
shop_table_by_location_id = {**shop_table_by_location_id, **{0x400020+cnt: x for cnt, x in enumerate(flat_retro_shops)}} shop_table_by_location_id = {**shop_table_by_location_id, **{0x400020+cnt: x for cnt, x in enumerate(flat_retro_shops)}}
shop_table_by_location = {y: x for x, y in shop_table_by_location_id.items()} shop_table_by_location = {y: x for x, y in shop_table_by_location_id.items()}
key_drop_data = {
'Hyrule Castle - Map Guard Key Drop': [0x140036, 0x140037, 'in Hyrule Castle', 'Small Key (Escape)'],
'Hyrule Castle - Boomerang Guard Key Drop': [0x140033, 0x140034, 'in Hyrule Castle', 'Small Key (Escape)'],
'Hyrule Castle - Key Rat Key Drop': [0x14000c, 0x14000d, 'in Hyrule Castle', 'Small Key (Escape)'],
'Hyrule Castle - Big Key Drop': [0x14003c, 0x14003d, 'in Hyrule Castle', 'Big Key (Escape)'],
'Eastern Palace - Dark Square Pot Key': [0x14005a, 0x14005b, 'in Eastern Palace', 'Small Key (Eastern Palace)'],
'Eastern Palace - Dark Eyegore Key Drop': [0x140048, 0x140049, 'in Eastern Palace', 'Small Key (Eastern Palace)'],
'Desert Palace - Desert Tiles 1 Pot Key': [0x140030, 0x140031, 'in Desert Palace', 'Small Key (Desert Palace)'],
'Desert Palace - Beamos Hall Pot Key': [0x14002a, 0x14002b, 'in Desert Palace', 'Small Key (Desert Palace)'],
'Desert Palace - Desert Tiles 2 Pot Key': [0x140027, 0x140028, 'in Desert Palace', 'Small Key (Desert Palace)'],
'Castle Tower - Dark Archer Key Drop': [0x140060, 0x140061, 'in Castle Tower', 'Small Key (Agahnims Tower)'],
'Castle Tower - Circle of Pots Key Drop': [0x140051, 0x140052, 'in Castle Tower', 'Small Key (Agahnims Tower)'],
'Swamp Palace - Pot Row Pot Key': [0x140018, 0x140019, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
'Swamp Palace - Trench 1 Pot Key': [0x140015, 0x140016, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
'Swamp Palace - Hookshot Pot Key': [0x140012, 0x140013, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
'Swamp Palace - Trench 2 Pot Key': [0x14000f, 0x140010, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
'Swamp Palace - Waterway Pot Key': [0x140009, 0x14000a, 'in Swamp Palace', 'Small Key (Swamp Palace)'],
'Skull Woods - West Lobby Pot Key': [0x14002d, 0x14002e, 'in Skull Woods', 'Small Key (Skull Woods)'],
'Skull Woods - Spike Corner Key Drop': [0x14001b, 0x14001c, 'near Mothula', 'Small Key (Skull Woods)'],
"Thieves' Town - Hallway Pot Key": [0x14005d, 0x14005e, "in Thieves Town", 'Small Key (Thieves Town)'],
"Thieves' Town - Spike Switch Pot Key": [0x14004e, 0x14004f, "in Thieves Town", 'Small Key (Thieves Town)'],
'Ice Palace - Jelly Key Drop': [0x140003, 0x140004, 'in Ice Palace', 'Small Key (Ice Palace)'],
'Ice Palace - Conveyor Key Drop': [0x140021, 0x140022, 'in Ice Palace', 'Small Key (Ice Palace)'],
'Ice Palace - Hammer Block Key Drop': [0x140024, 0x140025, 'in Ice Palace', 'Small Key (Ice Palace)'],
'Ice Palace - Many Pots Pot Key': [0x140045, 0x140046, 'in Ice Palace', 'Small Key (Ice Palace)'],
'Misery Mire - Spikes Pot Key': [0x140054, 0x140055 , 'in Misery Mire', 'Small Key (Misery Mire)'],
'Misery Mire - Fishbone Pot Key': [0x14004b, 0x14004c, 'in forgotten Mire', 'Small Key (Misery Mire)'],
'Misery Mire - Conveyor Crystal Key Drop': [0x140063, 0x140064 , 'in Misery Mire', 'Small Key (Misery Mire)'],
'Turtle Rock - Pokey 1 Key Drop': [0x140057, 0x140058, 'in Turtle Rock', 'Small Key (Turtle Rock)'],
'Turtle Rock - Pokey 2 Key Drop': [0x140006, 0x140007, 'in Turtle Rock', 'Small Key (Turtle Rock)'],
'Ganons Tower - Conveyor Cross Pot Key': [0x14003f, 0x140040, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
'Ganons Tower - Double Switch Pot Key': [0x140042, 0x140043, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
'Ganons Tower - Conveyor Star Pits Pot Key': [0x140039, 0x14003a, "in Ganon's Tower", 'Small Key (Ganons Tower)'],
'Ganons Tower - Mini Helmasaur Key Drop': [0x14001e, 0x14001f, "atop Ganon's Tower", 'Small Key (Ganons Tower)']
}
dungeon_events = [ dungeon_events = [
'Trench 1 Switch', 'Trench 1 Switch',
'Trench 2 Switch', 'Trench 2 Switch',
@@ -1276,6 +1324,7 @@ dungeon_events = [
'Suspicious Maiden', 'Suspicious Maiden',
'Revealing Light', 'Revealing Light',
'Ice Block Drop', 'Ice Block Drop',
'Skull Star Tile',
'Zelda Pickup', 'Zelda Pickup',
'Zelda Drop Off' 'Zelda Drop Off'
] ]
@@ -1517,18 +1566,19 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
'Suspicious Maiden': (None, None, False, None), 'Suspicious Maiden': (None, None, False, None),
'Revealing Light': (None, None, False, None), 'Revealing Light': (None, None, False, None),
'Ice Block Drop': (None, None, False, None), 'Ice Block Drop': (None, None, False, None),
'Skull Star Tile': (None, None, False, None),
'Zelda Pickup': (None, None, False, None), 'Zelda Pickup': (None, None, False, None),
'Zelda Drop Off': (None, None, False, None), 'Zelda Drop Off': (None, None, False, None),
'Eastern Palace - Prize': ([0x1209D, 0x53E76, 0x53E77, 0x180052, 0x180070, 0xC6FE], None, True, 'Eastern Palace'), 'Eastern Palace - Prize': ([0x1209D, 0x53E76, 0x53E77, 0x180052, 0x180070, 0xC6FE, 0x186FE2], None, True, 'Eastern Palace'),
'Desert Palace - Prize': ([0x1209E, 0x53E7A, 0x53E7B, 0x180053, 0x180072, 0xC6FF], None, True, 'Desert Palace'), 'Desert Palace - Prize': ([0x1209E, 0x53E7A, 0x53E7B, 0x180053, 0x180072, 0xC6FF, 0x186FE3], None, True, 'Desert Palace'),
'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706], None, True, 'Tower of Hera'), 'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706, 0x186FEA], None, True, 'Tower of Hera'),
'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702], None, True, 'Palace of Darkness'), 'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702, 0x186FE6], None, True, 'Palace of Darkness'),
'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701], None, True, 'Swamp Palace'), 'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701, 0x186FE5], None, True, 'Swamp Palace'),
'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707], None, True, 'Thieves\' Town'), 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707, 0x186FEB], None, True, 'Thieves Town'),
'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704], None, True, 'Skull Woods'), 'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704, 0x186FE8], None, True, 'Skull Woods'),
'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705], None, True, 'Ice Palace'), 'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705, 0x186FE9], None, True, 'Ice Palace'),
'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703], None, True, 'Misery Mire'), 'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703, 0x186FE7], None, True, 'Misery Mire'),
'Turtle Rock - Prize': ([0x120A7, 0x53E80, 0x53E81, 0x18005C, 0x180075, 0xC708], None, True, 'Turtle Rock'), 'Turtle Rock - Prize': ([0x120A7, 0x53E80, 0x53E81, 0x18005C, 0x180075, 0xC708, 0x186FEC], None, True, 'Turtle Rock'),
'Kakariko Shop - Left': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Left': (None, None, False, 'for sale in Kakariko'),
'Kakariko Shop - Middle': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Middle': (None, None, False, 'for sale in Kakariko'),
'Kakariko Shop - Right': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Right': (None, None, False, 'for sale in Kakariko'),
@@ -1564,8 +1614,6 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
} }
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
lookup_id_to_name.update(shop_table_by_location_id) lookup_id_to_name.update(shop_table_by_location_id)
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int} lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
lookup_name_to_id.update(shop_table_by_location) lookup_name_to_id.update(shop_table_by_location)
+252 -286
View File
@@ -1,6 +1,6 @@
import bisect import bisect
import collections
import io import io
import itertools
import json import json
import hashlib import hashlib
import logging import logging
@@ -15,7 +15,7 @@ try:
except ImportError: except ImportError:
raise Exception('Could not load BPS module') raise Exception('Could not load BPS module')
from BaseClasses import CollectionState, ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, PotItem from BaseClasses import ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, LocationType, Item
from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals
from Dungeons import dungeon_music_addresses, dungeon_table from Dungeons import dungeon_music_addresses, dungeon_table
from Regions import location_table, shop_to_location_table, retro_shops from Regions import location_table, shop_to_location_table, retro_shops
@@ -23,17 +23,22 @@ from RoomData import DoorKind
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
from Text import Uncle_texts, Ganon1_texts, Ganon_Phase_3_No_Silvers_texts, TavernMan_texts, Sahasrahla2_texts from Text import Uncle_texts, Ganon1_texts, Ganon_Phase_3_No_Silvers_texts, TavernMan_texts, Sahasrahla2_texts
from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import 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 Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts
from Text import LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts
from Text import 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 Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
from Items import ItemFactory from Items import ItemFactory
from EntranceShuffle import door_addresses, exit_ids, ow_prize_table from EntranceShuffle import door_addresses, exit_ids, ow_prize_table
from OverworldShuffle import default_flute_connections, flute_data from OverworldShuffle import default_flute_connections, flute_data
from InitialSram import InitialSram
from source.classes.SFX import randomize_sfx from source.classes.SFX import randomize_sfx
from source.item.FillUtil import valid_pot_items
from source.dungeon.RoomList import Room0127
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'fc6f1d6ba782d08ac92600c31cc7ee43' RANDOMIZERBASEHASH = '210e4631353e3d094f01bf91562844a5'
class JsonRom(object): class JsonRom(object):
@@ -44,6 +49,7 @@ class JsonRom(object):
self.orig_buffer = None self.orig_buffer = None
self.patches = {} self.patches = {}
self.addresses = [] self.addresses = []
self.initial_sram = InitialSram()
def write_byte(self, address, value): def write_byte(self, address, value):
self.write_bytes(address, [value]) self.write_bytes(address, [value])
@@ -73,6 +79,9 @@ class JsonRom(object):
del self.patches[str(intervalstart)] del self.patches[str(intervalstart)]
del self.addresses[pos] del self.addresses[pos]
def write_initial_sram(self):
self.write_bytes(0x183000, self.initial_sram.get_initial_sram())
def write_to_file(self, file): def write_to_file(self, file):
with open(file, 'w') as stream: with open(file, 'w') as stream:
json.dump([self.patches], stream) json.dump([self.patches], stream)
@@ -90,6 +99,7 @@ class LocalRom(object):
self.hash = hash self.hash = hash
self.orig_buffer = None self.orig_buffer = None
self.file = file self.file = file
self.initial_sram = InitialSram()
self.has_smc_header = False self.has_smc_header = False
if not os.path.isfile(file): if not os.path.isfile(file):
raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file) raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file)
@@ -105,6 +115,9 @@ class LocalRom(object):
def write_bytes(self, startaddress, values): def write_bytes(self, startaddress, values):
self.buffer[startaddress:startaddress + len(values)] = values self.buffer[startaddress:startaddress + len(values)] = values
def write_initial_sram(self):
self.write_bytes(0x183000, self.initial_sram.get_initial_sram())
def write_to_file(self, file): def write_to_file(self, file):
with open(file, 'wb') as outfile: with open(file, 'wb') as outfile:
outfile.write(self.buffer) outfile.write(self.buffer)
@@ -117,6 +130,13 @@ class LocalRom(object):
ret.write_bytes(int(address), values) ret.write_bytes(int(address), values)
return ret return ret
def verify_base_rom(self):
# verify correct checksum of baserom
basemd5 = hashlib.md5()
basemd5.update(self.buffer)
if JAP10HASH != basemd5.hexdigest():
raise RuntimeError('Supplied Base Rom does not match known MD5 for JAP(1.0) release.')
def patch_base_rom(self): def patch_base_rom(self):
# verify correct checksum of baserom # verify correct checksum of baserom
basemd5 = hashlib.md5() basemd5 = hashlib.md5()
@@ -163,7 +183,6 @@ class LocalRom(object):
with open(local_path('data/base2current.json'), 'w') as fp: with open(local_path('data/base2current.json'), 'w') as fp:
json.dump(patches, fp, separators=(',', ':')) json.dump(patches, fp, separators=(',', ':'))
def write_crc(self): def write_crc(self):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
inv = crc ^ 0xFFFF inv = crc ^ 0xFFFF
@@ -335,6 +354,9 @@ def patch_enemizer(world, player, rom, local_rom, enemizercli, random_sprite_on_
0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d]) 0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d])
rom.write_byte(0x200101, 0) # Do not close boss room door on entry. rom.write_byte(0x200101, 0) # Do not close boss room door on entry.
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. (for Ijwu's enemizer) rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. (for Ijwu's enemizer)
else:
rom.write_byte(0x04DE83, 0xB3) # maiden is now something else
if random_sprite_on_hit: if random_sprite_on_hit:
_populate_sprite_table() _populate_sprite_table()
@@ -549,6 +571,21 @@ class Sprite(object):
return array_chunk(palette_as_colors, 15) return array_chunk(palette_as_colors, 15)
def handle_native_dungeon(location, itemid):
# Keys in their native dungeon should use the original item code for keys
if location.parent_region.dungeon:
if location.parent_region.dungeon.name == location.item.dungeon:
if location.item.bigkey:
return 0x32
if location.item.smallkey:
return 0x24
if location.item.map:
return 0x33
if location.item.compass:
return 0x25
return itemid
def patch_rom(world, rom, player, team, enemized, is_mystery=False): def patch_rom(world, rom, player, team, enemized, is_mystery=False):
random.seed(world.rom_seeds[player]) random.seed(world.rom_seeds[player])
@@ -560,28 +597,46 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
distinguished_prog_bow_loc.item.code = 0x65 distinguished_prog_bow_loc.item.code = 0x65
# patch items # patch items
pot_mw_index = 0
for location in world.get_locations(): for location in world.get_locations():
if location.player != player: if location.player != player:
continue continue
itemid = location.item.code if location.item is not None else 0x5A itemid = location.item.code if location.item is not None else 0x5A
if location.type == LocationType.Pot:
if location.item.name in valid_pot_items and location.item.player == player:
location.pot.item = valid_pot_items[location.item.name]
else:
code = handle_native_dungeon(location, itemid)
standing_item_flag = 0x80
if location.item.player != player:
standing_item_flag |= 0x40
rom.write_byte(0x142800 + pot_mw_index * 2, code)
rom.write_byte(0x142800 + pot_mw_index * 2 + 1, location.item.player)
code = pot_mw_index
pot_mw_index += 1
location.pot.indicator = standing_item_flag
location.pot.standing_item_code = code
continue
elif location.type == LocationType.Drop:
if location.item.player != player:
code = 0xF9
else:
code = 0xF8
sprite_pointer = snes_to_pc(location.address)
rom.write_byte(sprite_pointer, handle_native_dungeon(location, itemid))
if code == 0xF9:
rom.write_byte(sprite_pointer+1, location.item.player)
else:
rom.write_byte(sprite_pointer+1, 0)
rom.write_byte(sprite_pointer+2, code)
continue
if location.address is None or (type(location.address) is int and location.address >= 0x400000): if location.address is None or (type(location.address) is int and location.address >= 0x400000):
continue continue
if not location.crystal: if not location.crystal:
if location.item is not None: if location.item is not None:
# Keys in their native dungeon should use the original item code for keys # Keys in their native dungeon should use the original item code for keys
if location.parent_region.dungeon: itemid = handle_native_dungeon(location, itemid)
if location.parent_region.dungeon.is_dungeon_item(location.item):
if location.item.bigkey:
itemid = 0x32
if location.item.smallkey:
itemid = 0x24
if location.item.map:
itemid = 0x33
if location.item.compass:
itemid = 0x25
if world.remote_items[player]: if world.remote_items[player]:
itemid = list(location_table.keys()).index(location.name) + 1 itemid = list(location_table.keys()).index(location.name) + 1
assert itemid < 0x100 assert itemid < 0x100
@@ -609,8 +664,25 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
if world.mapshuffle[player]: if world.mapshuffle[player]:
rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
if world.pottery[player] not in ['none']:
rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2A8000))
# make hammer pegs use different tiles
Room0127.write_to_rom(snes_to_pc(0x2A8000), rom)
if world.pot_contents[player]: if world.pot_contents[player]:
write_pots_to_rom(rom, world.pot_contents[player]) colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery']
and (world.colorizepots[player]
or world.pottery[player] in ['reduced', 'clustered']))
if world.pot_contents[player].size() > 0x2800:
raise Exception('Pot table is too big for current area')
world.pot_contents[player].write_pot_data_to_rom(rom, colorize_pots)
# fix for swamp drains if necessary
swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player)
if not swamp1location.pot.indicator:
rom.write_byte(0x142A53, 0)
swamp2location = world.get_location('Swamp Palace - Trench 2 Pot Key', player)
if not swamp2location.pot.indicator:
rom.write_byte(0x142A54, 0)
# patch flute spots # patch flute spots
owFlags = 0 owFlags = 0
@@ -775,7 +847,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
if world.doorShuffle[player] == 'crossed': if world.doorShuffle[player] == 'crossed':
dr_flags |= DROptions.Map_Info dr_flags |= DROptions.Map_Info
if world.experimental[player] and world.goal[player] not in ['triforcehunt', 'trinity']: if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']:
dr_flags |= DROptions.Debug dr_flags |= DROptions.Debug
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent': and world.mixed_travel[player] == 'prevent':
@@ -799,14 +871,20 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
if world.logic[player] != 'nologic': if world.logic[player] != 'nologic':
dr_flags |= DROptions.Fix_EG dr_flags |= DROptions.Fix_EG
my_locations = world.get_filled_locations(player)
valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item)
or (l.type == LocationType.Drop and not l.forced_item)
or (l.type == LocationType.Normal and not l.forced_item)
or (l.type == LocationType.Shop and world.shopsanity[player]))]
valid_loc_by_dungeon = valid_dungeon_locations(valid_locations)
# fix hc big key problems (map and compass too) # fix hc big key problems (map and compass too)
if world.doorShuffle[player] == 'crossed' or world.keydropshuffle[player]: if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player] or world.pottery[player] not in ['none', 'cave']:
rom.write_byte(0x151f1, 2) rom.write_byte(0x151f1, 2)
rom.write_byte(0x15270, 2) rom.write_byte(0x15270, 2)
sanctuary = world.get_region('Sanctuary', player) sanctuary = world.get_region('Sanctuary', player)
rom.write_byte(0x1597b, sanctuary.dungeon.dungeon_id*2) rom.write_byte(0x1597b, sanctuary.dungeon.dungeon_id*2)
update_compasses(rom, world, player) update_compasses(rom, valid_loc_by_dungeon, world, player)
def should_be_bunny(region, mode): def should_be_bunny(region, mode):
if mode != 'inverted': if mode != 'inverted':
@@ -839,9 +917,12 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used
rom.write_byte(0x13f030+offset, layout.max_chests) rom.write_byte(0x13f030+offset, layout.max_chests)
builder = world.dungeon_layouts[player][name] builder = world.dungeon_layouts[player][name]
rom.write_byte(0x13f080+offset, builder.location_cnt % 10) valid_cnt = len(valid_loc_by_dungeon[name])
rom.write_byte(0x13f090+offset, builder.location_cnt // 10) if valid_cnt > 99:
rom.write_byte(0x13f0a0+offset, builder.location_cnt) logging.getLogger('').warning(f'{name} exceeds 99 in locations ({valid_cnt})')
rom.write_byte(0x13f080+offset, valid_cnt % 10)
rom.write_byte(0x13f090+offset, valid_cnt // 10)
rom.write_byte(0x13f0a0+offset, valid_cnt)
bk_status = 1 if builder.bk_required else 0 bk_status = 1 if builder.bk_required else 0
bk_status = 2 if builder.bk_provided else bk_status bk_status = 2 if builder.bk_provided else bk_status
rom.write_byte(0x13f040+offset*2, bk_status) rom.write_byte(0x13f040+offset*2, bk_status)
@@ -924,33 +1005,53 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# bot: $7A is 1, 7B is 2, etc so 7D=4, 82=9 (zero unknown...) # bot: $7A is 1, 7B is 2, etc so 7D=4, 82=9 (zero unknown...)
return 0x53+int(num), 0x79+int(num) return 0x53+int(num), 0x79+int(num)
credits_total = 216 credits_total = len(valid_locations)
if world.keydropshuffle[player]:
credits_total += 33
if world.shopsanity[player]:
credits_total += 32
if world.retro[player]:
credits_total += 9 if world.shopsanity[player] else 5
if world.keydropshuffle[player]: if world.dropshuffle[player] or world.pottery[player] != 'none':
rom.write_byte(0x140000, 1) rom.write_byte(0x142A50, 1) # StandingItemsOn
multiClientFlags = ((0x1 if world.keydropshuffle[player] else 0) | (0x2 if world.shopsanity[player] else 0) multiClientFlags = ((0x1 if world.dropshuffle[player] else 0)
| (0x4 if world.retro[player] else 0)) | (0x2 if world.shopsanity[player] else 0)
rom.write_byte(0x140001, multiClientFlags) | (0x4 if world.retro[player] else 0)
| (0x8 if world.pottery[player] != 'none' else 0)
| (0x10 if is_mystery else 0))
rom.write_byte(0x142A51, multiClientFlags)
# StandingItemCounterMask
rom.write_byte(0x142A55, ((0x1 if world.pottery[player] not in ['none', 'cave'] else 0)
| (0x2 if world.dropshuffle[player] else 0)))
if world.pottery[player] not in ['none', 'keys']:
# Cuccos should not prevent kill rooms from opening
rom.write_byte(snes_to_pc(0x0DB457), 0x40)
rom.write_byte(snes_to_pc(0x28AA56), 0 if world.pottery[player] == 'none' else 1)
write_int16(rom, 0x187010, credits_total) # dynamic credits write_int16(rom, 0x187010, credits_total) # dynamic credits
if credits_total != 216: if credits_total != 216:
# collection rate address (hi): # collection rate address (hi):
cr_address = 0x238057 cr_address = 0x238055
cr_pc = cr_address - 0x120000 # convert to pc cr_pc = snes_to_pc(cr_address) # convert to pc
first_top, first_bot = credits_digit((credits_total // 100) % 10)
mid_top, mid_bot = credits_digit((credits_total // 10) % 10) mid_top, mid_bot = credits_digit((credits_total // 10) % 10)
last_top, last_bot = credits_digit(credits_total % 10) last_top, last_bot = credits_digit(credits_total % 10)
if credits_total >= 1000:
thousands_top, thousands_bot = credits_digit((credits_total // 1000) % 10)
rom.write_byte(cr_pc, 0xa2) # slash
rom.write_byte(cr_pc+1, thousands_top)
rom.write_byte(cr_pc+0x1e, 0xc2) # slash
rom.write_byte(cr_pc+0x1f, thousands_bot)
# modify stat config
stat_address = 0x23B969
stat_pc = snes_to_pc(stat_address)
rom.write_byte(stat_pc, 0xa9) # change to pos 21 (from b1)
rom.write_byte(stat_pc+2, 0xc0) # change to 12 bits (from a0)
rom.write_byte(stat_pc+3, 0x80) # change to four digits (from 60)
# top half # top half
rom.write_byte(cr_pc+0x1, mid_top) rom.write_byte(cr_pc+2, first_top)
rom.write_byte(cr_pc+0x2, last_top) rom.write_byte(cr_pc+3, mid_top)
rom.write_byte(cr_pc+4, last_top)
# bottom half # bottom half
rom.write_byte(cr_pc+0x1f, mid_bot) rom.write_byte(cr_pc+0x20, first_bot)
rom.write_byte(cr_pc+0x20, last_bot) rom.write_byte(cr_pc+0x21, mid_bot)
rom.write_byte(cr_pc+0x22, last_bot)
# patch medallion requirements # patch medallion requirements
if world.required_medallions[player][0] == 'Bombos': if world.required_medallions[player][0] == 'Bombos':
@@ -976,9 +1077,9 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# set open mode: # set open mode:
if world.mode[player] in ['open', 'inverted']: if world.mode[player] in ['open', 'inverted']:
rom.write_byte(0x180032, 0x01) # open mode init_open_mode_sram(rom)
elif world.mode[player] == 'standard': elif world.mode[player] == 'standard':
rom.write_byte(0x180032, 0x00) # standard mode init_standard_mode_sram(rom)
set_inverted_mode(world, player, rom, inverted_buffer) set_inverted_mode(world, player, rom, inverted_buffer)
@@ -1205,10 +1306,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# set swordless mode settings # set swordless mode settings
rom.write_byte(0x18003F, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer can harm ganon rom.write_byte(0x18003F, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer can harm ganon
rom.write_byte(0x180040, 0x01 if world.swords[player] == 'swordless' else 0x00) # open curtains
rom.write_byte(0x180041, 0x01 if world.swords[player] == 'swordless' else 0x00) # swordless medallions rom.write_byte(0x180041, 0x01 if world.swords[player] == 'swordless' else 0x00) # swordless medallions
rom.write_byte(0x180043, 0xFF if world.swords[player] == 'swordless' else 0x00) # starting sword for link
rom.write_byte(0x180044, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer activates tablets rom.write_byte(0x180044, 0x01 if world.swords[player] == 'swordless' else 0x00) # hammer activates tablets
if world.swords[player] == 'swordless':
rom.initial_sram.set_swordless_curtains() # open curtains
# set up clocks for timed modes # set up clocks for timed modes
if world.shuffle[player] == 'vanilla': if world.shuffle[player] == 'vanilla':
@@ -1224,45 +1325,46 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32)
write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32)
write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32)
write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32) rom.initial_sram.set_starting_timer(0) # starting time (in frames, sint32)
elif world.clock_mode == 'ohko': elif world.clock_mode == 'ohko':
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32)
write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32)
write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 0) # green clock adjustment time (in frames, sint32)
write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32) rom.initial_sram.set_starting_timer(0) # starting time (in frames, sint32)
elif world.clock_mode == 'countdown-ohko': elif world.clock_mode == 'countdown-ohko':
rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality
write_int32(rom, 0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, -100 * 60 * 60 * 60) # red clock adjustment time (in frames, sint32)
write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
if world.difficulty_adjustments[player] == 'normal': if world.difficulty_adjustments == 'normal':
write_int32(rom, 0x18020C, (10 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) rom.initial_sram.set_starting_timer((10 + ERtimeincrease) * 60) # starting time (in seconds)
else: else:
write_int32(rom, 0x18020C, int((5 + ERtimeincrease / 2) * 60 * 60)) # starting time (in frames, sint32) rom.initial_sram.set_starting_timer(int((5 + ERtimeincrease / 2) * 60)) # starting time (in seconds)
if world.clock_mode == 'stopwatch': if world.clock_mode == 'stopwatch':
rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode
write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32)
write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
write_int32(rom, 0x18020C, 0) # starting time (in frames, sint32) rom.initial_sram.set_starting_timer(0) # starting time (in frames, sint32)
if world.clock_mode == 'countdown': if world.clock_mode == 'countdown':
rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available
write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32) write_int32(rom, 0x180200, -2 * 60 * 60) # red clock adjustment time (in frames, sint32)
write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32) write_int32(rom, 0x180204, 2 * 60 * 60) # blue clock adjustment time (in frames, sint32)
write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32) write_int32(rom, 0x180208, 4 * 60 * 60) # green clock adjustment time (in frames, sint32)
write_int32(rom, 0x18020C, (40 + ERtimeincrease) * 60 * 60) # starting time (in frames, sint32) rom.initial_sram.set_starting_timer((40 + ERtimeincrease) * 60) # starting time (in seconds)
# set up goals for treasure hunt # set up goals for treasure hunt
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
if world.goal[player] in ['triforcehunt', 'trinity']: if world.goal[player] in ['triforcehunt', 'trinity']:
rom.write_byte(0x180167, int(world.treasure_hunt_count[player]) % 256) rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player]))
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
gametype = 0x04 # item gametype = 0x04 # item
if world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla' or world.keydropshuffle[player]: if (world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player]
or world.pottery[player] != 'none'):
gametype |= 0x02 # entrance/door gametype |= 0x02 # entrance/door
if enemized: if enemized:
gametype |= 0x01 # enemizer gametype |= 0x01 # enemizer
@@ -1282,14 +1384,16 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
rom.write_byte(0x180171, 0x01 if world.ganon_at_pyramid[player] else 0x00) # Enable respawning on pyramid after ganon death rom.write_byte(0x180171, 0x01 if world.ganon_at_pyramid[player] else 0x00) # Enable respawning on pyramid after ganon death
rom.write_byte(0x180173, 0x01) # Bob is enabled rom.write_byte(0x180173, 0x01) # Bob is enabled
rom.write_byte(0x180168, 0x08) # Spike Cave Damage rom.write_byte(0x180195, 0x08) # Spike Cave Damage
rom.write_bytes(0x18016B, [0x04, 0x02, 0x01]) # Set spike cave and MM spike room Byrna usage rom.write_bytes(0x18016B, [0x04, 0x02, 0x01]) # Set spike cave and MM spike room Byrna usage
rom.write_bytes(0x18016E, [0x04, 0x08, 0x10]) # Set spike cave and MM spike room Cape usage rom.write_bytes(0x18016E, [0x04, 0x08, 0x10]) # Set spike cave and MM spike room Cape usage
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
rom.write_byte(0x18008B, 0x01 if world.is_pyramid_open(player) else 0x00) # pre-open Pyramid Hole if world.open_pyramid[player]:
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.initial_sram.pre_open_pyramid_hole()
if world.crystals_needed_for_gt[player] == 0:
rom.initial_sram.pre_open_ganons_tower()
rom.write_byte(0x18008F, 0x01 if world.is_atgt_swapped(player) else 0x00) # AT/GT swapped rom.write_byte(0x18008F, 0x01 if world.is_atgt_swapped(player) else 0x00) # AT/GT swapped
rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable
@@ -1299,169 +1403,13 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00) rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00)
rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles
# starting overworld flags
assert len(world.initial_overworld_flags[player]) == 0x80 and all([i < 0x100 for i in world.initial_overworld_flags[player]])
rom.write_bytes(0x183280, world.initial_overworld_flags[player])
# Starting equipment # Starting equipment
if world.pseudoboots[player]: if world.pseudoboots[player]:
rom.write_byte(0x18008E, 0x01) rom.write_byte(0x18008E, 0x01)
rom.initial_sram.set_starting_equipment(world, player)
rom.write_byte(0x180034, 10 if not world.bombbag[player] else 0) # starting max bombs
rom.write_byte(0x180035, 30) # starting max arrows
equip = [0] * (0x340 + 0x4F)
equip[0x36C] = 0x18
equip[0x36D] = 0x18
equip[0x379] = 0x68
if world.bombbag[player]:
starting_max_bombs = 0
else:
starting_max_bombs = 10
starting_max_arrows = 30
startingstate = CollectionState(world)
if startingstate.has('Bow', player):
equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro[player]:
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
equip[0x38E] |= 0x40
if startingstate.has('Titans Mitts', player):
equip[0x354] = 2
elif startingstate.has('Power Glove', player):
equip[0x354] = 1
if startingstate.has('Golden Sword', player):
equip[0x359] = 4
elif startingstate.has('Tempered Sword', player):
equip[0x359] = 3
elif startingstate.has('Master Sword', player):
equip[0x359] = 2
elif startingstate.has('Fighter Sword', player):
equip[0x359] = 1
if startingstate.has('Mirror Shield', player):
equip[0x35A] = 3
elif startingstate.has('Red Shield', player):
equip[0x35A] = 2
elif startingstate.has('Blue Shield', player):
equip[0x35A] = 1
if startingstate.has('Red Mail', player):
equip[0x35B] = 2
elif startingstate.has('Blue Mail', player):
equip[0x35B] = 1
if startingstate.has('Magic Upgrade (1/4)', player):
equip[0x37B] = 2
equip[0x36E] = 0x80
elif startingstate.has('Magic Upgrade (1/2)', player):
equip[0x37B] = 1
equip[0x36E] = 0x80
for item in world.precollected_items:
if item.player != player:
continue
if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
'Titans Mitts', 'Power Glove', 'Progressive Glove',
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
'Red Mail', 'Blue Mail', 'Progressive Armor',
'Magic Upgrade (1/4)', 'Magic Upgrade (1/2)']:
continue
set_table = {'Book of Mudora': (0x34E, 1), 'Hammer': (0x34B, 1), 'Bug Catching Net': (0x34D, 1), 'Hookshot': (0x342, 1), 'Magic Mirror': (0x353, 2),
'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1),
'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)}
or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02),
'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20),
'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08),
'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20),
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10),
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20),
'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0),
'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08), 'Map (Agahnims Tower)': (0x369, 0x08),
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02),
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10),
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80),
'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04),
'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40),
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01), 'Ocarina (Activated)': (0x34C, 3, 0x38C, 0x01),
'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10),
'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)}
keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F],
'Small Key (Tower of Hera)': [0x386],
'Small Key (Agahnims Tower)': [0x380], 'Small Key (Palace of Darkness)': [0x382],
'Small Key (Thieves Town)': [0x387],
'Small Key (Skull Woods)': [0x384], 'Small Key (Swamp Palace)': [0x381],
'Small Key (Ice Palace)': [0x385],
'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388],
'Small Key (Ganons Tower)': [0x389],
'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]}
bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5,
'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8}
rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300}
bomb_caps = {'Bomb Upgrade (+5)': 5, 'Bomb Upgrade (+10)': 10}
arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10}
bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10}
arrows = {'Single Arrow': 1, 'Arrows (10)': 10}
if item.name in set_table:
equip[set_table[item.name][0]] = set_table[item.name][1]
elif item.name in or_table:
equip[or_table[item.name][0]] |= or_table[item.name][1]
elif item.name in set_or_table:
equip[set_or_table[item.name][0]] = set_or_table[item.name][1]
equip[set_or_table[item.name][2]] |= set_or_table[item.name][3]
elif item.name in keys:
for address in keys[item.name]:
equip[address] = min(equip[address] + 1, 99)
elif item.name in bottles:
if equip[0x34F] < world.difficulty_requirements[player].progressive_bottle_limit:
equip[0x35C + equip[0x34F]] = bottles[item.name]
equip[0x34F] += 1
elif item.name in rupees:
equip[0x360:0x362] = list(min(equip[0x360] + (equip[0x361] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
equip[0x362:0x364] = list(min(equip[0x362] + (equip[0x363] << 8) + rupees[item.name], 9999).to_bytes(2, byteorder='little', signed=False))
elif item.name in bomb_caps:
starting_max_bombs = min(starting_max_bombs + bomb_caps[item.name], 50)
elif item.name in arrow_caps:
starting_max_arrows = min(starting_max_arrows + arrow_caps[item.name], 70)
elif item.name in bombs:
equip[0x343] += bombs[item.name]
elif item.name in arrows:
if world.retro[player]:
equip[0x38E] |= 0x80
equip[0x377] = 1
else:
equip[0x377] += arrows[item.name]
elif item.name in ['Piece of Heart', 'Boss Heart Container', 'Sanctuary Heart Container']:
if item.name == 'Piece of Heart':
equip[0x36B] = (equip[0x36B] + 1) % 4
if item.name != 'Piece of Heart' or equip[0x36B] == 0:
equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0)
equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0)
else:
raise RuntimeError(f'Unsupported item in starting equipment: {item.name}')
equip[0x343] = min(equip[0x343], starting_max_bombs)
rom.write_byte(0x180034, starting_max_bombs)
equip[0x377] = min(equip[0x377], starting_max_arrows)
rom.write_byte(0x180035, starting_max_arrows)
rom.write_bytes(0x180046, equip[0x360:0x362])
if equip[0x359]:
rom.write_byte(0x180043, equip[0x359])
assert equip[:0x340] == [0] * 0x340
rom.write_bytes(0x183340, equip[0x340:])
rom.write_bytes(0x271A6, equip[0x340:0x340+60])
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
rom.write_byte(0x03A943, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both) rom.write_byte(0x03A943, 0xD0 if world.mode[player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
@@ -1523,7 +1471,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x18003C, 0x00) rom.write_byte(0x18003C, 0x00)
elif world.dungeon_counters[player] == 'on': elif world.dungeon_counters[player] == 'on':
compass_mode = 0x02 # always on compass_mode = 0x02 # always on
elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup': elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player]
or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']):
compass_mode = 0x01 # show on pickup compass_mode = 0x01 # show on pickup
if (world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default') \ if (world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default') \
or (world.owMixed[player] and not (world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default')): or (world.owMixed[player] and not (world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default')):
@@ -1574,25 +1523,27 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x53E36+ow_map_index*2, int16_as_bytes(coords[0])) rom.write_bytes(0x53E36+ow_map_index*2, int16_as_bytes(coords[0]))
rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1]))
rom.write_byte(0x53EA6+ow_map_index, world_indicator) rom.write_byte(0x53EA6+ow_map_index, world_indicator)
# in crossed doors - flip the compass exists flags # in crossed doors - flip the compass exists flags
if world.doorShuffle[player] == 'crossed': if world.doorShuffle[player] == 'crossed':
exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') for dungeon, portal_list in dungeon_portals.items():
rom.write_byte(0x53E96+ow_map_index, 0x1 if exists_flag else 0x0) ow_map_index = dungeon_table[dungeon].map_index
exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass')
rom.write_byte(0x53E96+ow_map_index, 0x1 if exists_flag else 0x0)
rom.write_byte(0x18003C, compass_mode) rom.write_byte(0x18003C, compass_mode)
# Bitfield - enable free items to show up in menu # Bitfield - enable free items to show up in menu
# #
# ----dcba # ----dcba
# d - Compass # d - Compass
# c - Map # c - Map
# b - Big Key # b - Big Key
# a - Small Key # a - Small Key
# #
enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'none'
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] else 0x00) rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] else 0x00)
| (0x02 if world.bigkeyshuffle[player] else 0x00) | (0x02 if world.bigkeyshuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00)
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu | (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu
# Map reveals # Map reveals
@@ -1720,13 +1671,17 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
if world.doorShuffle[player] != 'vanilla' or world.keydropshuffle[player]: if (world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player]
or world.pottery[player] != 'none'):
for room in world.rooms: for room in world.rooms:
if room.player == player and room.modified: if room.player == player and room.modified:
rom.write_bytes(room.address(), room.rom_data()) rom.write_bytes(room.address(), room.rom_data())
write_strings(rom, world, player, team) write_strings(rom, world, player, team)
# write initial sram
rom.write_initial_sram()
rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0) rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0)
# set rom name # set rom name
@@ -1831,14 +1786,16 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite,
ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx): ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx, msu_resume):
if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer: 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], dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380],
rom.orig_buffer[0xdedf5:0xdedf9], "data/sprites/official/001.link.1.zspr", "Nintendo", "Link") rom.orig_buffer[0xdedf5:0xdedf9], "data/sprites/official/001.link.1.zspr", "Nintendo", "Link")
# todo: implement a flag for msu resume delay if msu_resume:
rom.write_bytes(0x18021D, [0, 0]) # default to off for now rom.write_bytes(0x18021D, [0x8, 0x7])
else:
rom.write_bytes(0x18021D, [0, 0]) # default to off for now
if sprite and not isinstance(sprite, Sprite): if sprite and not isinstance(sprite, Sprite):
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite) sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
@@ -2151,6 +2108,8 @@ def write_strings(rom, world, player, team):
else: else:
if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon: if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon:
hint = dest.dungeon.name hint = dest.dungeon.name
elif isinstance(dest, Item) and world.experimental[player]:
hint = f'{{C:RED}}{dest.hint_text}{{C:WHITE}}' if dest.hint_text else 'something'
else: else:
hint = dest.hint_text if dest.hint_text else "something" hint = dest.hint_text if dest.hint_text else "something"
if dest.player != player: if dest.player != player:
@@ -2170,7 +2129,7 @@ def write_strings(rom, world, player, team):
all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player] all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player]
random.shuffle(all_entrances) random.shuffle(all_entrances)
#First we take care of the one inconvenient dungeon in the appropriately simple shuffles. # First we take care of the one inconvenient dungeon in the appropriately simple shuffles.
entrances_to_hint = {} entrances_to_hint = {}
entrances_to_hint.update(InconvenientDungeonEntrances) entrances_to_hint.update(InconvenientDungeonEntrances)
if world.shuffle_ganon: if world.shuffle_ganon:
@@ -2185,7 +2144,7 @@ def write_strings(rom, world, player, team):
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
entrances_to_hint = {} entrances_to_hint = {}
break break
#Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
entrances_to_hint.update(InconvenientOtherEntrances) entrances_to_hint.update(InconvenientOtherEntrances)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
hint_count = 0 hint_count = 0
@@ -2203,7 +2162,7 @@ def write_strings(rom, world, player, team):
else: else:
break break
#Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. # Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle.
if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']:
entrances_to_hint.update(ConnectorEntrances) entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances) entrances_to_hint.update(DungeonEntrances)
@@ -2262,7 +2221,7 @@ def write_strings(rom, world, player, team):
else: else:
second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item)
first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item)
this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.') this_hint = f'The westmost chests in Swamp Palace contain {first_item} and {second_item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Mire Left': elif location == 'Mire Left':
if random.randint(0, 1) == 0: if random.randint(0, 1) == 0:
@@ -2271,34 +2230,43 @@ def write_strings(rom, world, player, team):
else: else:
second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item)
first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item)
this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.') this_hint = f'The westmost chests in Misery Mire contain {first_item} and {second_item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Tower of Hera - Big Key Chest': elif location == 'Tower of Hera - Big Key Chest':
this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'Waiting in the Tower of Hera basement leads to {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Ganons Tower - Big Chest': elif location == 'Ganons Tower - Big Chest':
this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'The big chest in Ganon\'s Tower contains {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Thieves\' Town - Big Chest': elif location == 'Thieves\' Town - Big Chest':
this_hint = 'The big chest in Thieves\' Town contains ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'The big chest in Thieves\' Town contains {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Ice Palace - Big Chest': elif location == 'Ice Palace - Big Chest':
this_hint = 'The big chest in Ice Palace contains ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'The big chest in Ice Palace contains {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Eastern Palace - Big Key Chest': elif location == 'Eastern Palace - Big Key Chest':
this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'The antifairy guarded chest in Eastern Palace contains {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Sahasrahla': elif location == 'Sahasrahla':
this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'Sahasrahla seeks a green pendant for {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
elif location == 'Graveyard Cave': elif location == 'Graveyard Cave':
this_hint = 'The cave north of the graveyard contains ' + hint_text(world.get_location(location, player).item) + '.' item = hint_text(world.get_location(location, player).item)
this_hint = f'The cave north of the graveyard contains {item}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
else: else:
this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' this_hint = f'{location} contains {hint_text(world.get_location(location, player).item)}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
# 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. # 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() items_to_hint = RelevantItems.copy()
flute_item = 'Ocarina' flute_item = 'Ocarina'
if world.is_tile_swapped(0x18, player): if world.is_tile_swapped(0x18, player):
@@ -2326,8 +2294,10 @@ def write_strings(rom, world, player, team):
this_location = world.find_items_not_key_only(this_item, player) this_location = world.find_items_not_key_only(this_item, player)
random.shuffle(this_location) random.shuffle(this_location)
if this_location: if this_location:
this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' item_name = this_location[0].item.hint_text
this_hint = this_hint[0].upper() + this_hint[1:] item_name = item_name[0].upper() + item_name[1:]
item_format = f'{{C:RED}}{item_name}{{C:WHITE}}' if world.experimental[player] else item_name
this_hint = f'{item_format} can be found {hint_text(this_location[0])}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
hint_count -= 1 hint_count -= 1
@@ -2381,7 +2351,8 @@ def write_strings(rom, world, player, team):
elif hint_type == 'path': elif hint_type == 'path':
if item_count == 1: if item_count == 1:
the_item = text_for_item(next(iter(choice_set)), world, player, team) the_item = text_for_item(next(iter(choice_set)), world, player, team)
hint_candidates.append((hint_type, f'{name} conceals {the_item}')) item_format = f'{{C:RED}}{the_item}{{C:WHITE}}' if world.experimental[player] else the_item
hint_candidates.append((hint_type, f'{name} conceals only {item_format}'))
else: else:
hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items')) hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items'))
district_hints = min(len(hint_candidates), len(hint_locations)) district_hints = min(len(hint_candidates), len(hint_locations))
@@ -2468,7 +2439,7 @@ def write_strings(rom, world, player, team):
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!' tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
else: else:
if world.goal[player] in ['trinity']: if world.goal[player] == 'trinity':
trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player] trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player]
tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player]) tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player])
@@ -2569,6 +2540,16 @@ def text_for_item(item, world, player, team):
else: else:
return f'{item.hint_text} for {world.player_names[item.player][team]}' return f'{item.hint_text} for {world.player_names[item.player][team]}'
def init_open_mode_sram(rom):
rom.initial_sram.pre_open_castle_gate()
rom.initial_sram.set_progress_indicator(0x02)
rom.initial_sram.set_progress_flags(0x14)
rom.initial_sram.set_starting_entrance(0x01)
def init_standard_mode_sram(rom):
rom.initial_sram.set_progress_indicator(0x00)
rom.initial_sram.set_progress_flags(0x0)
rom.initial_sram.set_starting_entrance(0x00)
def set_inverted_mode(world, player, rom, inverted_buffer): def set_inverted_mode(world, player, rom, inverted_buffer):
if world.mode[player] == 'inverted': if world.mode[player] == 'inverted':
@@ -2780,7 +2761,7 @@ def patch_shuffled_dark_sanc(world, rom, player):
dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name) dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name)
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1]
door_index = door_addresses[str(dark_sanc_entrance)][0] door_index = door_addresses[str(dark_sanc_entrance)][0]
world.initial_overworld_flags[player][ow_area] |= door_addresses[dark_sanc_entrance][2] rom.initial_sram.pre_set_overworld_flag(ow_area, door_addresses[dark_sanc_entrance][2])
rom.write_byte(0x180241, 0x01) rom.write_byte(0x180241, 0x01)
rom.write_byte(0x180248, door_index + 1) rom.write_byte(0x180248, door_index + 1)
@@ -2795,7 +2776,7 @@ def patch_shuffled_bomb_shop(world, rom, player):
bomb_shop_entrance = str([i for i in bomb_shop.entrances if i.parent_region.name != 'Menu'][0].name) bomb_shop_entrance = str([i for i in bomb_shop.entrances if i.parent_region.name != 'Menu'][0].name)
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[bomb_shop_entrance][1] room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[bomb_shop_entrance][1]
door_index = door_addresses[str(bomb_shop_entrance)][0] door_index = door_addresses[str(bomb_shop_entrance)][0]
world.initial_overworld_flags[player][ow_area] |= door_addresses[bomb_shop_entrance][2] rom.initial_sram.pre_set_overworld_flag(ow_area, door_addresses[bomb_shop_entrance][2])
rom.write_byte(0x180240, 0x02) rom.write_byte(0x180240, 0x02)
rom.write_byte(0x180247, door_index + 1) rom.write_byte(0x180247, door_index + 1)
@@ -2806,12 +2787,23 @@ def patch_shuffled_bomb_shop(world, rom, player):
write_int16(rom, snes_to_pc(0x02D996), door_1) write_int16(rom, snes_to_pc(0x02D996), door_1)
def update_compasses(rom, world, player): def valid_dungeon_locations(valid_locations):
dungeon_locations = collections.defaultdict(set)
for l in valid_locations:
if l.parent_region.dungeon:
dungeon_locations[l.parent_region.dungeon.name].add(l)
return dungeon_locations
def update_compasses(rom, dungeon_locations, world, player):
layouts = world.dungeon_layouts[player] layouts = world.dungeon_layouts[player]
provided_dungeon = False provided_dungeon = False
for name, builder in layouts.items(): for name, builder in layouts.items():
dungeon_id = compass_data[name][4] dungeon_id = compass_data[name][4]
rom.write_byte(0x187000 + dungeon_id//2, builder.location_cnt) dungeon_count = len(dungeon_locations[name])
if dungeon_count > 255:
logging.getLogger('').warning(f'{name} has more locations than 255. Need 16-bit compass counts')
rom.write_byte(0x187000 + dungeon_id//2, dungeon_count % 256)
if builder.bk_provided: if builder.bk_provided:
if provided_dungeon: if provided_dungeon:
logging.getLogger('').warning('Multiple dungeons have forced BKs! Compass code might need updating?') logging.getLogger('').warning('Multiple dungeons have forced BKs! Compass code might need updating?')
@@ -2929,7 +2921,7 @@ OtherEntrances = {'Lake Hylia Fairy': 'A cave NE of Lake Hylia',
'Kakariko Gamble Game': 'The old Kakariko gambling den', 'Kakariko Gamble Game': 'The old Kakariko gambling den',
'Bonk Fairy (Light)': 'The rock pile near your home', 'Bonk Fairy (Light)': 'The rock pile near your home',
'Hookshot Fairy': 'The left paired cave on east DM', 'Hookshot Fairy': 'The left paired cave on east DM',
'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop', 'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop',
'Dark Lake Hylia Fairy': 'The cave NE dark Lake Hylia', 'Dark Lake Hylia Fairy': 'The cave NE dark Lake Hylia',
'Dark Death Mountain Fairy': 'The SW cave on dark DM', 'Dark Death Mountain Fairy': 'The SW cave on dark DM',
'East Dark World Hint': 'The dark cave near the eastmost portal', 'East Dark World Hint': 'The dark cave near the eastmost portal',
@@ -3085,29 +3077,3 @@ hash_alphabet = [
"Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key" "Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
] ]
pot_item_room_table_lookup = 0xDB67
###
# Pointer to pot location and contents for each non-empty pot in a supertile
# Format: [(x, y, item)] FF FF (Note: x,y are bit packed to include layer)
pot_item_table = 0xDDE7
pot_item_table_end = 0xE6B0
def write_pots_to_rom(rom, pot_contents):
n = pot_item_table
rom.write_bytes(n, [0xFF,0xFF])
n += 2
for i in range(0x140):
if i in 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, 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:
write_int16(rom, pot_item_room_table_lookup + 2*i, n-2)
else:
write_int16(rom, pot_item_room_table_lookup + 2*i, n-2)
assert n <= pot_item_table_end
+120 -41
View File
@@ -3,7 +3,8 @@ import logging
from collections import deque from collections import deque
import OverworldGlitchRules import OverworldGlitchRules
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType, LocationType
from BaseClasses import PotFlags
from Dungeons import dungeon_table from Dungeons import dungeon_table
from RoomData import DoorKind from RoomData import DoorKind
from OverworldGlitchRules import overworld_glitches_rules from OverworldGlitchRules import overworld_glitches_rules
@@ -33,6 +34,7 @@ def set_rules(world, player):
raise NotImplementedError('Not implemented yet') raise NotImplementedError('Not implemented yet')
bomb_rules(world, player) bomb_rules(world, player)
pot_rules(world, player)
if world.logic[player] == 'noglitches': if world.logic[player] == 'noglitches':
no_glitches_rules(world, player) no_glitches_rules(world, player)
@@ -56,9 +58,10 @@ def set_rules(world, player):
elif world.goal[player] == 'ganon': elif world.goal[player] == 'ganon':
# require aga2 to beat ganon # require aga2 to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player)) add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
if world.goal[player] in ['triforcehunt', 'trinity']: elif world.goal[player] in ['triforcehunt', 'trinity']:
if ('Murahdahla', player) in world._location_cache: for location in world.get_region('Hyrule Castle Courtyard', player).locations:
add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) if location.name == 'Murahdahla':
add_rule(location, lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
# if swamp and dam have not been moved we require mirror for swamp palace # if swamp and dam have not been moved we require mirror for swamp palace
if not world.swamp_patch_required[player]: if not world.swamp_patch_required[player]:
@@ -213,6 +216,9 @@ def global_rules(world, player):
set_rule(world.get_location('Hookshot Cave - Bottom Right', player), lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player)) set_rule(world.get_location('Hookshot Cave - Bottom Right', player), lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Hookshot Cave Bonk Path', player), lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Hookshot Cave Hook Path', player), lambda state: state.has('Hookshot', player))
# Start of door rando rules # Start of door rando rules
# TODO: Do these need to flag off when door rando is off? - some of them, yes # TODO: Do these need to flag off when door rando is off? - some of them, yes
@@ -277,6 +283,7 @@ def global_rules(world, player):
set_rule(world.get_entrance('Swamp Trench 1 Departure Key', player), lambda state: state.has('Flippers', player) and state.has('Trench 1 Filled', player)) set_rule(world.get_entrance('Swamp Trench 1 Departure Key', player), lambda state: state.has('Flippers', player) and state.has('Trench 1 Filled', player))
set_rule(world.get_location('Trench 1 Switch', player), lambda state: state.has('Hammer', player)) set_rule(world.get_location('Trench 1 Switch', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Swamp Hub Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Swamp Hub Hook Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Swamp Hub Side Hook Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Swamp Trench 2 Pots Dry', player), lambda state: not state.has('Trench 2 Filled', player)) set_rule(world.get_entrance('Swamp Trench 2 Pots Dry', player), lambda state: not state.has('Trench 2 Filled', player))
set_rule(world.get_entrance('Swamp Trench 2 Pots Wet', player), lambda state: state.has('Flippers', player) and state.has('Trench 2 Filled', player)) set_rule(world.get_entrance('Swamp Trench 2 Pots Wet', player), lambda state: state.has('Flippers', player) and state.has('Trench 2 Filled', player))
@@ -300,6 +307,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player) or state.has('Hidden Pits', player))
set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), lambda state: state.has('Hidden Pits', player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player))
@@ -312,8 +321,9 @@ def global_rules(world, player):
# I think these rules are unnecessary now - testing needed # I think these rules are unnecessary now - testing needed
# for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']: # for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
# forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player) # forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player)
for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']: # forbid_item(world.get_location('Thieves\' Town - Blind\'s Cell', player), 'Big Key (Thieves Town)', player)
set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player)) # for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']:
# set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player))
set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player)) set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player))
set_rule(world.get_location('Thieves\' Town - Boss', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Boss', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Thieves\' Town - Boss', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Boss', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state))
@@ -345,6 +355,9 @@ def global_rules(world, player):
state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ...
# byrna could work with sufficient magic # byrna could work with sufficient magic
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
loc = world.get_location('Misery Mire - Spikes Pot Key', player)
if loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area
set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player))
@@ -361,13 +374,19 @@ def global_rules(world, player):
set_rule(world.get_entrance('TR Hub EN', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub EN', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Hub NW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub NW', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Hub NE', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub NE', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Hub Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Hub Ledges Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player))
set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Final Abyss South Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride Ledges Path', player), lambda state: state.has('Cane of Somaria', player))
for location in world.get_region('TR Dark Ride Ledges', player).locations:
set_rule(location, lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Final Abyss Balcony Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Final Abyss Ledge Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
@@ -381,12 +400,12 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hookshot', player))
if not world.get_door('GT Speed Torch SE', player).entranceFlag: if not world.get_door('GT Speed Torch SE', player).entranceFlag:
set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('GT Hookshot East-North Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('GT Hookshot South-East Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('GT Hookshot South-North Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot East-Mid Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
set_rule(world.get_entrance('GT Hookshot East-South Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) set_rule(world.get_entrance('GT Hookshot North-Mid Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
set_rule(world.get_entrance('GT Hookshot North-East Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) set_rule(world.get_entrance('GT Hookshot Mid-South Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
set_rule(world.get_entrance('GT Hookshot North-South Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) set_rule(world.get_entrance('GT Hookshot Mid-East Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
set_rule(world.get_entrance('GT Firesnake Room Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Firesnake Room Hook Path', player), lambda state: state.has('Hookshot', player))
# I am tempted to stick an invincibility rule for getting across falling bridge # I am tempted to stick an invincibility rule for getting across falling bridge
@@ -415,6 +434,9 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player))
# todo: the following only applies to crystal state propagation from this supertile
# you can also reset the supertile, but I'm not sure how to model that
set_rule(world.get_entrance('GT Falling Torches Down Ladder', player), lambda state: state.has_Boots(player))
set_rule(world.get_entrance('GT Moldorm Gap', player), lambda state: state.has('Hookshot', player) and world.get_region('GT Moldorm', player).dungeon.bosses['top'].can_defeat(state)) set_rule(world.get_entrance('GT Moldorm Gap', player), lambda state: state.has('Hookshot', player) and world.get_region('GT Moldorm', player).dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
@@ -423,6 +445,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player)) set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player))
else: else:
set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player)) set_rule(world.get_entrance('Thieves Attic ES', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player))
set_rule(world.get_entrance('Thieves Attic Orange Barrier', player), lambda state: state.can_reach_orange(world.get_region('Thieves Attic', player), player))
set_rule(world.get_entrance('Thieves Attic Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Thieves Attic', player), player))
set_rule(world.get_entrance('Hera Lobby to Front Barrier - Blue', player), lambda state: state.can_reach_blue(world.get_region('Hera Lobby', player), player)) set_rule(world.get_entrance('Hera Lobby to Front Barrier - Blue', player), lambda state: state.can_reach_blue(world.get_region('Hera Lobby', player), player))
set_rule(world.get_entrance('Hera Front to Lobby Barrier - Blue', player), lambda state: state.can_reach_blue(world.get_region('Hera Front', player), player)) set_rule(world.get_entrance('Hera Front to Lobby Barrier - Blue', player), lambda state: state.can_reach_blue(world.get_region('Hera Front', player), player))
@@ -446,6 +470,7 @@ def global_rules(world, player):
set_rule(world.get_entrance('Hera Basement Cage to Crystal', player), lambda state: state.can_hit_crystal(player)) set_rule(world.get_entrance('Hera Basement Cage to Crystal', player), lambda state: state.can_hit_crystal(player))
set_rule(world.get_entrance('Hera Tridorm to Crystal', player), lambda state: state.can_hit_crystal(player)) set_rule(world.get_entrance('Hera Tridorm to Crystal', player), lambda state: state.can_hit_crystal(player))
set_rule(world.get_entrance('Hera Startile Wide to Crystal', player), lambda state: state.can_hit_crystal(player)) set_rule(world.get_entrance('Hera Startile Wide to Crystal', player), lambda state: state.can_hit_crystal(player))
set_rule(world.get_entrance('Hera 5F Orange Path', player), lambda state: state.can_reach_orange(world.get_region('Hera 5F', player), player))
set_rule(world.get_entrance('PoD Arena North to Landing Barrier - Orange', player), lambda state: state.can_reach_orange(world.get_region('PoD Arena North', player), player)) set_rule(world.get_entrance('PoD Arena North to Landing Barrier - Orange', player), lambda state: state.can_reach_orange(world.get_region('PoD Arena North', player), player))
set_rule(world.get_entrance('PoD Arena Landing to North Barrier - Orange', player), lambda state: state.can_reach_orange(world.get_region('PoD Arena Landing', player), player)) set_rule(world.get_entrance('PoD Arena Landing to North Barrier - Orange', player), lambda state: state.can_reach_orange(world.get_region('PoD Arena Landing', player), player))
@@ -615,10 +640,13 @@ def global_rules(world, player):
def bomb_rules(world, player): def bomb_rules(world, player):
# todo: kak well, pod hint (bonkable pots), hookshot pot, spike cave pots
bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended. bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended.
bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Light Hype Fairy', 'Mini Moldorm Cave', bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave',
'Hookshot Cave Back to Middle', 'Hookshot Cave Front to Middle', 'Hookshot Cave Middle to Front','Hookshot Cave Middle to Back', 'Hookshot Cave Back to Middle', 'Hookshot Cave Front to Middle', 'Hookshot Cave Middle to Front',
'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery', 'Light Hype Fairy'] 'Hookshot Cave Middle to Back', 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery',
'Paradox Cave Chest Area NE', 'Blinds Hideout N', 'Kakariko Well (top to back)',
'Light Hype Fairy']
for entrance in bonkable_doors: for entrance in bonkable_doors:
add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player)) add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player))
add_bunny_rule(world.get_entrance(entrance, player), player) add_bunny_rule(world.get_entrance(entrance, player), player)
@@ -627,8 +655,7 @@ def bomb_rules(world, player):
add_bunny_rule(world.get_entrance(entrance, player), player) add_bunny_rule(world.get_entrance(entrance, player), player)
bonkable_items = ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right'] bonkable_items = ['Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right']
bombable_items = ['Blind\'s Hideout - Top', 'Kakariko Well - Top', 'Chicken House', 'Aginah\'s Cave', 'Graveyard Cave', bombable_items = ['Chicken House', 'Aginah\'s Cave', 'Graveyard Cave',
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right',
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom'] 'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom']
for location in bonkable_items: for location in bonkable_items:
add_rule(world.get_location(location, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player)) add_rule(world.get_location(location, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player))
@@ -732,6 +759,53 @@ def bomb_rules(world, player):
elif door.kind(world) in [DoorKind.Bombable]: elif door.kind(world) in [DoorKind.Bombable]:
add_rule(door.entrance, lambda state: state.can_use_bombs(player)) add_rule(door.entrance, lambda state: state.can_use_bombs(player))
def pot_rules(world, player):
if world.pottery[player] != 'none':
blocks = [l for l in world.get_locations() if l.type == LocationType.Pot and l.pot.flags & PotFlags.Block]
for block_pot in blocks:
add_rule(block_pot, lambda state: state.can_lift_rocks(player))
for l in world.get_region('Hookshot Fairy', player).locations:
if l.type == LocationType.Pot:
add_rule(l, lambda state: state.has('Hookshot', player))
for l in world.get_region('Spike Cave', player).locations:
if l.type == LocationType.Pot:
add_rule(l, lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and
((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or
(state.has('Cane of Byrna', player) and
(state.can_extend_magic(player, 12, True) or
(state.world.can_take_damage and (state.has_Boots(player) or state.has_hearts(player, 4)))))))
for l in world.get_region('Dark Desert Hint', player).locations:
if l.type == LocationType.Pot:
add_rule(l, lambda state: state.can_use_bombs(player))
for l in world.get_region('Palace of Darkness Hint', player).locations:
if l.type == LocationType.Pot:
add_rule(l, lambda state: state.can_use_bombs(player) or state.has_Boots(player))
for number in ['1', '2']:
loc = world.get_location_unsafe(f'Dark Lake Hylia Ledge Spike Cave Pot #{number}', player)
if loc and loc.type == LocationType.Pot:
add_rule(loc, lambda state: state.world.can_take_damage or state.has('Hookshot', player)
or state.has('Cape', player)
or (state.has('Cane of Byrna', player)
and state.world.difficulty_adjustments[player] == 'normal'))
for l in world.get_region('Ice Hammer Block', player).locations:
if l.type == LocationType.Pot:
add_rule(l, lambda state: state.has('Hammer', player) and state.can_lift_rocks(player))
for pot in ['Ice Antechamber Pot #3', 'Ice Antechamber Pot #4']:
loc = world.get_location_unsafe(pot, player)
if loc:
set_rule(loc, lambda state: state.has('Hammer', player) and state.can_lift_rocks(player))
loc = world.get_location_unsafe('Mire Spikes Pot #3', player)
if loc:
set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4))
or state.has('Cane of Byrna', player) or state.has('Cape', player))
for l in world.get_region('Ice Refill', player).locations:
if l.type == LocationType.Pot:
# or can_reach_blue is redundant as you have to hit a crystal switch somewhere...
add_rule(l, lambda state: state.can_hit_crystal(player))
def default_rules(world, player): def default_rules(world, player):
set_rule(world.get_entrance('Other World S&Q', player), lambda state: state.has_Mirror(player) and state.has('Beat Agahnim 1', player)) set_rule(world.get_entrance('Other World S&Q', player), lambda state: state.has_Mirror(player) and state.has('Beat Agahnim 1', player))
@@ -828,7 +902,7 @@ def default_rules(world, 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 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('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)) set_rule(world.get_entrance('Zora Waterfall Water Approach', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Kakariko Pond Whirlpool', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Kakariko Pond Whirlpool', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('River Bend Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('River Bend Water Drop', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('River Bend East Water Drop', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('River Bend East Water Drop', player), lambda state: state.has('Flippers', player))
@@ -1035,7 +1109,7 @@ def ow_rules(world, player):
if not world.is_tile_swapped(0x1b, player): if not world.is_tile_swapped(0x1b, player):
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: False) 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('Inverted Pyramid Entrance', player), lambda state: False)
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.open_pyramid[player] or world.goal[player] == 'trinity' or state.has('Beat Agahnim 2', player)) set_rule(world.get_entrance('Pyramid Hole', player), lambda state: world.open_pyramid[player] or state.has('Beat Agahnim 2', player))
set_rule(world.get_entrance('HC Area Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Area Mirror Spot', player), lambda state: state.has_Mirror(player))
set_rule(world.get_entrance('HC Ledge Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Ledge Mirror Spot', player), lambda state: state.has_Mirror(player))
@@ -1346,7 +1420,7 @@ def ow_bunny_rules(world, player):
add_bunny_rule(world.get_entrance('Zora Waterfall Water Drop', player), player) add_bunny_rule(world.get_entrance('Zora Waterfall Water Drop', player), player)
add_bunny_rule(world.get_entrance('Zora Waterfall Water Entry', player), player) add_bunny_rule(world.get_entrance('Zora Waterfall Water Entry', player), player)
add_bunny_rule(world.get_entrance('Waterfall of Wishing Cave Entry', player), player) add_bunny_rule(world.get_entrance('Zora Waterfall Water Approach', player), player)
add_bunny_rule(world.get_entrance('Kakariko Pond Whirlpool', player), player) add_bunny_rule(world.get_entrance('Kakariko Pond Whirlpool', player), player)
add_bunny_rule(world.get_entrance('River Bend Water Drop', player), player) add_bunny_rule(world.get_entrance('River Bend Water Drop', player), player)
add_bunny_rule(world.get_entrance('River Bend East Water Drop', player), player) add_bunny_rule(world.get_entrance('River Bend East Water Drop', player), player)
@@ -1385,6 +1459,7 @@ def ow_bunny_rules(world, player):
add_bunny_rule(world.get_entrance('Bomber Corner Water Drop', 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) add_bunny_rule(world.get_entrance('Bomber Corner Waterfall Water Drop', player), player)
def no_glitches_rules(world, player): def no_glitches_rules(world, player):
# todo: move some dungeon rules to no glictes logic - see these for examples # todo: move some dungeon rules to no glictes logic - see these for examples
# add_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) # add_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
@@ -1399,7 +1474,7 @@ def no_glitches_rules(world, player):
def fake_flipper_rules(world, player): def fake_flipper_rules(world, player):
set_rule(world.get_entrance('Waterfall of Wishing Cave Entry', player), lambda state: True) # warning, assumes FF possible on other end of whirlpool or local ancilla splash delete set_rule(world.get_entrance('Zora Waterfall Water Approach', player), lambda state: True) # warning, assumes FF possible on other end of whirlpool or local ancilla splash delete
set_rule(world.get_entrance('River Bend Water Drop', player), lambda state: True) set_rule(world.get_entrance('River Bend Water Drop', player), lambda state: True)
set_rule(world.get_entrance('River Bend East Water Drop', player), lambda state: True) set_rule(world.get_entrance('River Bend East Water Drop', player), lambda state: True)
set_rule(world.get_entrance('Potion Shop Water Drop', player), lambda state: True) set_rule(world.get_entrance('Potion Shop Water Drop', player), lambda state: True)
@@ -1418,7 +1493,7 @@ def fake_flipper_rules(world, player):
set_rule(world.get_entrance('Hype Cave Water Entry', player), lambda state: True) set_rule(world.get_entrance('Hype Cave Water Entry', player), lambda state: True)
set_rule(world.get_entrance('Ice Lake Southeast Water Drop', player), lambda state: True) set_rule(world.get_entrance('Ice Lake Southeast Water Drop', player), lambda state: True)
set_rule(world.get_entrance('Bomber Corner Water Drop', player), lambda state: True) set_rule(world.get_entrance('Bomber Corner Water Drop', player), lambda state: True)
add_bunny_rule(world.get_entrance('Waterfall of Wishing Cave Entry', player), player) add_bunny_rule(world.get_entrance('Zora Waterfall Water Approach', player), player)
add_bunny_rule(world.get_entrance('River Bend Water Drop', player), player) add_bunny_rule(world.get_entrance('River Bend Water Drop', player), player)
add_bunny_rule(world.get_entrance('River Bend East Water Drop', player), player) add_bunny_rule(world.get_entrance('River Bend East Water Drop', player), player)
add_bunny_rule(world.get_entrance('Potion Shop Water Drop', player), player) add_bunny_rule(world.get_entrance('Potion Shop Water Drop', player), player)
@@ -1466,7 +1541,8 @@ def add_conditional_lamps(world, player):
add_lamp_requirement(spot, player) add_lamp_requirement(spot, player)
dark_rooms = { dark_rooms = {
'TR Dark Ride': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride SW'], 'locations': []}, 'TR Dark Ride': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Dark Ride Path'], 'locations': []},
'TR Dark Ride Ledges': {'sewer': False, 'entrances': ['TR Dark Ride Ledges Path'], 'locations': []},
'Mire Dark Shooters': {'sewer': False, 'entrances': ['Mire Dark Shooters Up Stairs', 'Mire Dark Shooters SW', 'Mire Dark Shooters SE'], 'locations': []}, 'Mire Dark Shooters': {'sewer': False, 'entrances': ['Mire Dark Shooters Up Stairs', 'Mire Dark Shooters SW', 'Mire Dark Shooters SE'], 'locations': []},
'Mire Key Rupees': {'sewer': False, 'entrances': ['Mire Key Rupees NE'], 'locations': []}, 'Mire Key Rupees': {'sewer': False, 'entrances': ['Mire Key Rupees NE'], 'locations': []},
'Mire Block X': {'sewer': False, 'entrances': ['Mire Block X NW', 'Mire Block X WS'], 'locations': []}, 'Mire Block X': {'sewer': False, 'entrances': ['Mire Block X NW', 'Mire Block X WS'], 'locations': []},
@@ -1499,6 +1575,10 @@ def add_conditional_lamps(world, player):
'Sewers Rope Room': {'sewer': True, 'entrances': ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs'], 'locations': []}, 'Sewers Rope Room': {'sewer': True, 'entrances': ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs'], 'locations': []},
'Sewers Water': {'sewer': True, 'entrances': ['Sewers Water S', 'Sewers Water W'], 'locations': []}, 'Sewers Water': {'sewer': True, 'entrances': ['Sewers Water S', 'Sewers Water W'], 'locations': []},
'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat E', 'Sewers Key Rat Key Door N'], 'locations': ['Hyrule Castle - Key Rat Key Drop']}, 'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat E', 'Sewers Key Rat Key Door N'], 'locations': ['Hyrule Castle - Key Rat Key Drop']},
'Old Man Cave': {'sewer': False, 'entrances': ['Old Man Cave Exit (East)']},
'Old Man House Back': {'sewer': False, 'entrances': ['Old Man House Back to Front', 'Old Man House Exit (Top)']},
'Death Mountain Return Cave (left)': {'sewer': False, 'entrances': ['Death Mountain Return Cave E', 'Death Mountain Return Cave Exit (West)']},
'Death Mountain Return Cave (right)': {'sewer': False, 'entrances': ['Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave W']},
} }
dark_debug_set = set() dark_debug_set = set()
@@ -1515,17 +1595,12 @@ def add_conditional_lamps(world, player):
dark_debug_set.add(region) dark_debug_set.add(region)
for ent in info['entrances']: for ent in info['entrances']:
add_conditional_lamp(ent, region, 'Entrance') add_conditional_lamp(ent, region, 'Entrance')
for loc in info['locations']: r = world.get_region(region, player)
for loc in r.locations:
add_conditional_lamp(loc, region, 'Location') add_conditional_lamp(loc, region, 'Location')
logging.getLogger('').debug('Non Dark Regions: ' + ', '.join(set(dark_rooms.keys()).difference(dark_debug_set))) logging.getLogger('').debug('Non Dark Regions: ' + ', '.join(set(dark_rooms.keys()).difference(dark_debug_set)))
add_conditional_lamp('Old Man', 'Old Man Cave', 'Location')
add_conditional_lamp('Old Man Cave Exit (East)', 'Old Man Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave', 'Entrance')
add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance') add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance')
add_conditional_lamp('Old Man House Back to Front', 'Old Man House', 'Entrance')
def open_rules(world, player): def open_rules(world, player):
# softlock protection as you can reach the sewers small key door with a guard drop key # softlock protection as you can reach the sewers small key door with a guard drop key
@@ -1593,8 +1668,8 @@ def add_connection(parent_name, target_name, entrance_name, world, player):
def standard_rules(world, player): def standard_rules(world, player):
add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player) add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
world.get_entrance('Uncle S&Q', player).hide_path = True world.get_entrance('Uncle S&Q', player).hide_path = True
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player) and state.has('Zelda Delivered', player)) set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player) and state.has('Zelda Delivered', player)) set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.has('Zelda Delivered', player))
# these are because of rails # these are because of rails
if world.shuffle[player] != 'vanilla': if world.shuffle[player] != 'vanilla':
# where ever these happen to be # where ever these happen to be
@@ -1602,6 +1677,8 @@ def standard_rules(world, player):
entrance = world.get_portal(portal_name, player).door.entrance entrance = world.get_portal(portal_name, player).door.entrance
set_rule(entrance, lambda state: state.has('Zelda Delivered', player)) set_rule(entrance, lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player)) set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Hyrule Castle Ledge Drop', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has('Zelda Delivered', player))
# zelda should be saved before agahnim is in play # zelda should be saved before agahnim is in play
add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
@@ -1870,7 +1947,7 @@ bunny_revivable_entrances = {
"Ice Many Pots", "Mire South Fish", "Mire Right Bridge", "Mire Left Bridge", "Ice Many Pots", "Mire South Fish", "Mire Right Bridge", "Mire Left Bridge",
"TR Boss", "Eastern Hint Tile Blocked Path", "Thieves Spike Switch", "TR Boss", "Eastern Hint Tile Blocked Path", "Thieves Spike Switch",
"Thieves Boss", "Mire Spike Barrier", "Mire Cross", "Mire Hidden Shooters", "Thieves Boss", "Mire Spike Barrier", "Mire Cross", "Mire Hidden Shooters",
"Mire Spikes", "TR Final Abyss", "TR Dark Ride", "TR Pokey 1", "TR Tile Room", "Mire Spikes", "TR Final Abyss Balcony", "TR Dark Ride", "TR Pokey 1", "TR Tile Room",
"TR Roller Room", "Eastern Cannonball", "Thieves Hallway", "Ice Switch Room", "TR Roller Room", "Eastern Cannonball", "Thieves Hallway", "Ice Switch Room",
"Mire Tile Room", "Mire Conveyor Crystal", "Mire Hub", "TR Dash Bridge", "Mire Tile Room", "Mire Conveyor Crystal", "Mire Hub", "TR Dash Bridge",
"TR Hub", "Eastern Boss", "Eastern Lobby", "Thieves Ambush", "TR Hub", "Eastern Boss", "Eastern Lobby", "Thieves Ambush",
@@ -1934,15 +2011,17 @@ bunny_impassible_doors = {
'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier', 'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
'Mire South Fish Blue Barrier', 'Mire Tile Room NW', 'Mire Compass Blue Barrier', 'Mire Attic Hint Hole', 'Mire South Fish Blue Barrier', 'Mire Tile Room NW', 'Mire Compass Blue Barrier', 'Mire Attic Hint Hole',
'Mire Dark Shooters SW', 'Mire Crystal Mid Blue Barrier', 'Mire Crystal Left Blue Barrier', 'TR Main Lobby Gap', 'Mire Dark Shooters SW', 'Mire Crystal Mid Blue Barrier', 'Mire Crystal Left Blue Barrier', 'TR Main Lobby Gap',
'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Torches NW', 'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path',
'TR Pokey 2 Bottom to Top Barrier - Blue', 'TR Pokey 2 Top to Bottom Barrier - Blue', 'TR Twin Pokeys SW', 'TR Twin Pokeys EN', 'TR Big Chest Gap', 'TR Hub Ledges Path', 'TR Torches NW', 'TR Pokey 2 Bottom to Top Barrier - Blue',
'TR Pokey 2 Top to Bottom Barrier - Blue', 'TR Twin Pokeys SW', 'TR Twin Pokeys EN', 'TR Big Chest Gap',
'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride Up Stairs', 'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride Up Stairs',
'TR Dark Ride SW', 'TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze End to Interior Barrier - Blue', 'TR Dark Ride SW', 'TR Dark Ride Path', 'TR Dark Ride Ledges Path',
'TR Final Abyss South Stairs', 'TR Final Abyss NW', 'GT Hope Room EN', 'GT Blocked Stairs Block Path', 'TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze End to Interior Barrier - Blue',
'TR Final Abyss Balcony Path', 'TR Final Abyss Ledge Path', 'GT Hope Room EN', 'GT Blocked Stairs Block Path',
'GT Bob\'s Room Hole', 'GT Speed Torch SE', 'GT Speed Torch South Path', 'GT Speed Torch North Path', 'GT Bob\'s Room Hole', 'GT Speed Torch SE', 'GT Speed Torch South Path', 'GT Speed Torch North Path',
'GT Crystal Conveyor NE', 'GT Crystal Conveyor WN', 'GT Conveyor Cross EN', 'GT Conveyor Cross WN', 'GT Crystal Conveyor NE', 'GT Crystal Conveyor WN', 'GT Conveyor Cross EN', 'GT Conveyor Cross WN',
'GT Hookshot East-North Path', 'GT Hookshot East-South Path', 'GT Hookshot North-East Path', 'GT Hookshot East-Mid Path', 'GT Hookshot South-Mid Path', 'GT Hookshot North-Mid Path',
'GT Hookshot North-South Path', 'GT Hookshot South-East Path', 'GT Hookshot South-North Path', 'GT Hookshot Mid-South Path', 'GT Hookshot Mid-East Path', 'GT Hookshot Mid-North Path',
'GT Hookshot Platform Blue Barrier', 'GT Hookshot Entry Blue Barrier', 'GT Double Switch Pot Corners to Exit Barrier - Blue', 'GT Hookshot Platform Blue Barrier', 'GT Hookshot Entry Blue Barrier', 'GT Double Switch Pot Corners to Exit Barrier - Blue',
'GT Double Switch Exit to Blue Barrier', 'GT Firesnake Room Hook Path', 'GT Falling Bridge WN', 'GT Falling Bridge WS', 'GT Double Switch Exit to Blue Barrier', 'GT Firesnake Room Hook Path', 'GT Falling Bridge WN', 'GT Falling Bridge WS',
'GT Ice Armos NE', 'GT Ice Armos WS', 'GT Crystal Paths SW', 'GT Mimics 1 NW', 'GT Mimics 1 ES', 'GT Mimics 2 WS', 'GT Ice Armos NE', 'GT Ice Armos WS', 'GT Crystal Paths SW', 'GT Mimics 1 NW', 'GT Mimics 1 ES', 'GT Mimics 2 WS',
+1 -1
View File
@@ -45,7 +45,7 @@ def main(args=None):
test("Vanilla ", "--shuffle vanilla") test("Vanilla ", "--shuffle vanilla")
test("Retro ", "--retro --shuffle vanilla") test("Retro ", "--retro --shuffle vanilla")
test("Keysanity ", "--shuffle vanilla --keydropshuffle --keysanity") test("Keysanity ", "--shuffle vanilla --keydropshuffle drops_only --keysanity")
test("Shopsanity", "--shuffle vanilla --shopsanity") test("Shopsanity", "--shuffle vanilla --shopsanity")
test("Simple ", "--shuffle simple") test("Simple ", "--shuffle simple")
test("Full ", "--shuffle full") test("Full ", "--shuffle full")
+42 -11
View File
@@ -1,6 +1,7 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from collections import OrderedDict from collections import OrderedDict
import logging import logging
import re
text_addresses = {'Pedestal': (0x180300, 256), text_addresses = {'Pedestal': (0x180300, 256),
'Triforce': (0x180400, 256), 'Triforce': (0x180400, 256),
@@ -625,11 +626,16 @@ class MultiByteCoreTextMapper(object):
"{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03], "{IBOX}": [0x6B, 0x02, 0x77, 0x07, 0x7A, 0x03],
"{C:GREEN}": [0x77, 0x07], "{C:GREEN}": [0x77, 0x07],
"{C:YELLOW}": [0x77, 0x02], "{C:YELLOW}": [0x77, 0x02],
"{C:WHITE}": [0x77, 0x06],
"{C:INV_WHITE}": [0x77, 0x16],
"{C:INV_YELLOW}": [0x77, 0x12],
"{C:INV_GREEN}": [0x77, 0x17],
"{C:RED}": [0x77, 0x01],
"{C:INV_RED}": [0x77, 0x11],
} }
@classmethod @classmethod
def convert(cls, text, pause=True, wrap=19): def convert(cls, text, pause=True, wrap=19):
#text = text.upper()
lines = text.split('\n') lines = text.split('\n')
outbuf = bytearray() outbuf = bytearray()
lineindex = 0 lineindex = 0
@@ -639,7 +645,9 @@ class MultiByteCoreTextMapper(object):
while lines: while lines:
linespace = wrap linespace = wrap
line = lines.pop(0) line = lines.pop(0)
if line.startswith('{'):
match = re.search('^\{[A-Z0-9_:]+\}$', line)
if match:
if line == '{PAGEBREAK}': if line == '{PAGEBREAK}':
if lineindex % 3 != 0: if lineindex % 3 != 0:
# insert a wait for keypress, unless we just did so # insert a wait for keypress, unless we just did so
@@ -656,9 +664,27 @@ class MultiByteCoreTextMapper(object):
pending_space = False pending_space = False
while words: while words:
word = words.pop(0) word = words.pop(0)
# sanity check: if the word we have is more than 19 characters, we take as much as we can still fit and push the rest back for later
match = re.search('^(\{[A-Z0-9_:]+\}).*', word)
if match:
start_command = match.group(1)
outbuf.extend(cls.special_commands[start_command])
word = word.replace(start_command, '')
match = re.search('(\{[A-Z0-9_:]+\})\.?$', word)
if match:
end_command = match.group(1)
word = word.replace(end_command, '')
period = word.endswith('.')
else:
end_command, period = None, False
# sanity check: if the word we have is more than 19 characters,
# we take as much as we can still fit and push the rest back for later
if cls.wordlen(word) > wrap: if cls.wordlen(word) > wrap:
(word_first, word_rest) = cls.splitword(word, linespace) (word_first, word_rest) = cls.splitword(word, linespace)
if end_command:
word_rest = (word_rest[:-1] + end_command + '.') if period else (word_rest + end_command)
words.insert(0, word_rest) words.insert(0, word_rest)
lines.insert(0, ' '.join(words)) lines.insert(0, ' '.join(words))
@@ -671,9 +697,16 @@ class MultiByteCoreTextMapper(object):
if cls.wordlen(word) < linespace: if cls.wordlen(word) < linespace:
pending_space = True pending_space = True
linespace -= cls.wordlen(word) + 1 if pending_space else 0 linespace -= cls.wordlen(word) + 1 if pending_space else 0
outbuf.extend(RawMBTextMapper.convert(word)) word_to_map = word[:-1] if period else word
outbuf.extend(RawMBTextMapper.convert(word_to_map))
if end_command:
outbuf.extend(cls.special_commands[end_command])
if period:
outbuf.extend(RawMBTextMapper.convert('.'))
else: else:
# ran out of space, push word and lines back and continue with next line # ran out of space, push word and lines back and continue with next line
if end_command:
word = (word[:-1] + end_command + '.') if period else (word + end_command)
words.insert(0, word) words.insert(0, word)
lines.insert(0, ' '.join(words)) lines.insert(0, ' '.join(words))
break break
@@ -778,7 +811,7 @@ class CharTextMapper(object):
@classmethod @classmethod
def map_char(cls, char): def map_char(cls, char):
if cls.number_offset is not None: if cls.number_offset is not None:
if 0x30 <= ord(char) <= 0x39: if 0x30 <= ord(char) <= 0x39:
return ord(char) + cls.number_offset return ord(char) + cls.number_offset
if 0x41 <= ord(char) <= 0x5A: if 0x41 <= ord(char) <= 0x5A:
return ord(char) + 0x20 + cls.alpha_offset return ord(char) + 0x20 + cls.alpha_offset
@@ -789,7 +822,6 @@ class CharTextMapper(object):
@classmethod @classmethod
def convert(cls, text): def convert(cls, text):
buf = bytearray() buf = bytearray()
#for char in text.lower():
for char in text: for char in text:
buf.append(cls.map_char(char)) buf.append(cls.map_char(char))
return buf return buf
@@ -1257,7 +1289,6 @@ class RawMBTextMapper(CharTextMapper):
@classmethod @classmethod
def convert(cls, text): def convert(cls, text):
buf = bytearray() buf = bytearray()
#for char in text.lower():
for char in text: for char in text:
res = cls.map_char(char) res = cls.map_char(char)
if isinstance(res, int): if isinstance(res, int):
@@ -1743,7 +1774,7 @@ class TextTable(object):
text['telepathic_tile_misery_mire'] = CompressedTextMapper.convert("{NOBORDER}\nLighting 4 torches will open your way forward!") text['telepathic_tile_misery_mire'] = CompressedTextMapper.convert("{NOBORDER}\nLighting 4 torches will open your way forward!")
text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^") text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^")
text['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.") text['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.")
text['telepathic_tile_under_ganon'] = CompressedTextMapper.convert("Secondary tournament winners\n{HARP}\n ~~~2021~~~\nmatt7898\n\n ~~~2020~~~\nrelkin") text['telepathic_tile_under_ganon'] = CompressedTextMapper.convert("Doors Async League winners\n{HARP}\n ~~~2022~~~\nAndy\n\n ~~~2021~~~\nprdwong")
text['telepathic_tile_palace_of_darkness'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a funny looking Enemizer") text['telepathic_tile_palace_of_darkness'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a funny looking Enemizer")
# C0 # C0
text['telepathic_tile_desert_bonk_torch_room'] = CompressedTextMapper.convert("{NOBORDER}\nThings can be knocked down, if you fancy yourself a dashing dude.") text['telepathic_tile_desert_bonk_torch_room'] = CompressedTextMapper.convert("{NOBORDER}\nThings can be knocked down, if you fancy yourself a dashing dude.")
@@ -1753,7 +1784,7 @@ class TextTable(object):
text['telepathic_tile_ice_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nYou can use Fire Rod or Bombos to pass.") text['telepathic_tile_ice_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nYou can use Fire Rod or Bombos to pass.")
text['telepathic_tile_ice_stalfos_knights_room'] = CompressedTextMapper.convert("{NOBORDER}\nKnock 'em down and then bomb them dead.") text['telepathic_tile_ice_stalfos_knights_room'] = CompressedTextMapper.convert("{NOBORDER}\nKnock 'em down and then bomb them dead.")
text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.") text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.")
text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n ~~~2021~~~\ndaaanty\n\n ~~~2019~~~\nJet082") text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n ~~~2021~~~\nDaaanty\n\n ~~~2019~~~\nJet082\n\n ~~~2018~~~\nAndy\n\n ~~~2017~~~\nA: ajneb174\nS: ajneb174")
text['caught_a_bee'] = CompressedTextMapper.convert("Caught a Bee\n ≥ Keep\n Release\n{CHOICE}") text['caught_a_bee'] = CompressedTextMapper.convert("Caught a Bee\n ≥ Keep\n Release\n{CHOICE}")
text['caught_a_fairy'] = CompressedTextMapper.convert("Caught Fairy!\n ≥ Keep\n Release\n{CHOICE}") text['caught_a_fairy'] = CompressedTextMapper.convert("Caught Fairy!\n ≥ Keep\n Release\n{CHOICE}")
text['no_empty_bottles'] = CompressedTextMapper.convert("Whoa, bucko!\nNo empty bottles.") text['no_empty_bottles'] = CompressedTextMapper.convert("Whoa, bucko!\nNo empty bottles.")
@@ -1981,7 +2012,7 @@ class TextTable(object):
text['ganon_fall_in_alt'] = CompressedTextMapper.convert("You think you are ready to face me?\n\nI will not die unless you complete your goals. Dingus!") text['ganon_fall_in_alt'] = CompressedTextMapper.convert("You think you are ready to face me?\n\nI will not die unless you complete your goals. Dingus!")
text['ganon_phase_3_alt'] = CompressedTextMapper.convert("Got wax in your ears? I cannot die!") text['ganon_phase_3_alt'] = CompressedTextMapper.convert("Got wax in your ears? I cannot die!")
# 190 # 190
text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("How did you get up here?") text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("Glitched\ntournament\nwinners\n{HARP}\n~~~HMG 2021~~~\nKrithel\n\n~~~OWG 2019~~~\nGlan\n\n~~~OWG 2018~~~\nChristosOwen\nthe numpty")
text['fish_money'] = CompressedTextMapper.convert("It's a secret to everyone.") text['fish_money'] = CompressedTextMapper.convert("It's a secret to everyone.")
text['sign_ganons_tower'] = CompressedTextMapper.convert("You need all 7 crystals to enter.") text['sign_ganons_tower'] = CompressedTextMapper.convert("You need all 7 crystals to enter.")
text['sign_ganon'] = CompressedTextMapper.convert("You need all 7 crystals to beat Ganon.") text['sign_ganon'] = CompressedTextMapper.convert("You need all 7 crystals to beat Ganon.")
@@ -1991,4 +2022,4 @@ class TextTable(object):
text['ganon_phase_3_silvers'] = CompressedTextMapper.convert("Oh no! Silver! My one true weakness!") text['ganon_phase_3_silvers'] = CompressedTextMapper.convert("Oh no! Silver! My one true weakness!")
text['murahdahla'] = CompressedTextMapper.convert("Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n{PAUSE3}\n… … …\nWait! you can see me? I knew I should have\nhidden in a hollow tree.") text['murahdahla'] = CompressedTextMapper.convert("Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n{PAUSE3}\n… … …\nWait! you can see me? I knew I should have\nhidden in a hollow tree.")
text['end_pad_data'] = bytearray([0xfb]) text['end_pad_data'] = bytearray([0xfb])
text['terminator'] = bytearray([0xFF, 0xFF]) text['terminator'] = bytearray([0xFF, 0xFF])
+50 -19
View File
@@ -585,10 +585,11 @@ def extract_data_from_jp_rom(rom):
with open(rom, 'rb') as stream: with open(rom, 'rb') as stream:
rom_data = bytearray(stream.read()) rom_data = bytearray(stream.read())
rooms = [0x1c, 0x1d, 0x4e] rooms = range(0, 0x128)
# rooms = [0x7b, 0x7c, 0x7d, 0x8b, 0x8c, 0x8d, 0x9b, 0x9c, 0x9d] # rooms = [0x7b, 0x7c, 0x7d, 0x8b, 0x8c, 0x8d, 0x9b, 0x9c, 0x9d]
# rooms = [0x1a, 0x2a, 0xd1] # rooms = [0x1a, 0x2a, 0xd1]
for room in rooms: for room in rooms:
# print(f'Room {room:02x}')
b2idx = room*2 b2idx = room*2
b3idx = room*3 b3idx = room*3
headerptr = 0x271e2 + b2idx headerptr = 0x271e2 + b2idx
@@ -605,6 +606,7 @@ def extract_data_from_jp_rom(rom):
stop, idx = False, 0 stop, idx = False, 0
ffcnt = 0 ffcnt = 0
mode = 0 mode = 0
secret_cnt = 0
# first two bytes # first two bytes
b1 = rom_data[objectloc+idx] b1 = rom_data[objectloc+idx]
b2 = rom_data[objectloc+idx+1] b2 = rom_data[objectloc+idx+1]
@@ -626,9 +628,20 @@ def extract_data_from_jp_rom(rom):
idx += 2 idx += 2
elif not mode and ffcnt < 3: elif not mode and ffcnt < 3:
objectdata.append(b3) objectdata.append(b3)
if b3 == 0xFA and ((b2 & 0x3) << 2) | (b1 & 0x3) in [0xf, 0xc]:
# potcalc
vram = ((b1 & 0xFC) >> 1) | ((b2 & 0xFC) << 5)
low = vram & 0xFF
high = (vram & 0xFF00) >> 8
if ffcnt == 1:
high |= 0x20
print(f'db {", ".join([f"${low:02x}", f"${high:02x}"])}')
secret_cnt += 1
idx += 3 idx += 3
else: else:
idx += 2 idx += 2
if secret_cnt:
print(f'Room {room:02x} {secret_cnt}')
spriteptr = 0x4d62e + b2idx spriteptr = 0x4d62e + b2idx
spriteloc = rom_data[spriteptr] + rom_data[spriteptr+1]*0x100 + 0x40000 spriteloc = rom_data[spriteptr] + rom_data[spriteptr+1]*0x100 + 0x40000
secretptr = 0xdb67 + b2idx secretptr = 0xdb67 + b2idx
@@ -652,23 +665,40 @@ def extract_data_from_jp_rom(rom):
else: else:
secretdata.append(b3) secretdata.append(b3)
idx += 3 idx += 3
print(f'Room {room:02x}') # print(f'Room {room:02x}')
print(f'HeaderPtr {headerptr:06x}') # print(f'HeaderPtr {headerptr:06x}')
print(f'HeaderLoc {headerloc:06x}') # print(f'HeaderLoc {headerloc:06x}')
print(f'db {",".join([f"${x:02x}" for x in header])}') # print(f'db {",".join([f"${x:02x}" for x in header])}')
print(f'Obj Length: {len(objectdata)}') # print(f'Obj Length: {len(objectdata)}')
print(f'ObjectPtr {objectptr:06x}') # print(f'ObjectPtr {objectptr:06x}')
print(f'ObjectLoc {objectloc:06x}') # print(f'ObjectLoc {objectloc:06x}')
for i in range(0, len(objectdata)//16 + 1): # for i in range(0, len(objectdata)//16 + 1):
slice = objectdata[i*16:i*16+16] # slice = objectdata[i*16:i*16+16]
print(f'db {",".join([f"${x:02x}" for x in slice])}') # print(f'db {",".join([f"${x:02x}" for x in slice])}')
print(f'SpritePtr {spriteptr:06x}') # print(f'SpritePtr {spriteptr:06x}')
print(f'SpriteLoc {spriteloc:06x}') # print(f'SpriteLoc {spriteloc:06x}')
print_data_block(spritedata) # print_data_block(spritedata)
print(f'SecretPtr {secretptr:06x}') # print(f'SecretPtr {secretptr:06x}')
print(f'SecretLoc {secretloc:06x}') # print(f'SecretLoc {secretloc:06x}')
print_data_block(secretdata) # print_data_block(secretdata)
print() # print()
def count_set_bits(val):
if val == 0:
return 0
else:
return (val & 1) + count_set_bits(val >> 1)
def check_pots():
from PotShuffle import vanilla_pots
for supertile, pot_list in vanilla_pots.items():
for i,pot in enumerate(pot_list):
if pot.obj_ref:
r = pot.obj_ref
secret_vram = pot.x | (pot.y << 8)
tile_vram = ((r.data[1] & 0xFC) << 5) | ((r.data[0] & 0xFC) >> 1)
if secret_vram != tile_vram:
print(f'{pot.room}#{i+1} secret: {hex(secret_vram)} tile: {hex(tile_vram)}')
def stack_size3a(size=2): def stack_size3a(size=2):
@@ -712,4 +742,5 @@ if __name__ == '__main__':
# read_entrance_data(old_rom=sys.argv[1]) # read_entrance_data(old_rom=sys.argv[1])
# room_palette_data(old_rom=sys.argv[1]) # room_palette_data(old_rom=sys.argv[1])
# extract_data_from_us_rom(sys.argv[1]) # extract_data_from_us_rom(sys.argv[1])
extract_data_from_jp_rom(sys.argv[1]) # extract_data_from_jp_rom(sys.argv[1])
check_pots()
-1
View File
@@ -44,5 +44,4 @@ incsrc doortables.asm
warnpc $288000 warnpc $288000
; deals with own hooks ; deals with own hooks
incsrc keydropshuffle.asm
incsrc owrando.asm incsrc owrando.asm
+1 -1
View File
@@ -176,7 +176,7 @@ org $02d9ce ; <- Bank02.asm : Dungeon_LoadEntrance 10829 (STA $A0 : STA $048E)
JSL CheckDarkWorldSpawn : NOP JSL CheckDarkWorldSpawn : NOP
org $01891e ; <- Bank 01.asm : 991 Dungeon_LoadType2Object (LDA $00 : XBA : AND.w #$00FF) org $01891e ; <- Bank 01.asm : 991 Dungeon_LoadType2Object (LDA $00 : XBA : AND.w #$00FF)
JSL RainPrevention : NOP #2 JSL RainPrevention : BCC + : RTS : NOP : +
org $1edabf ; <- sprite_energy_ball.asm : 86-7 Sprite_EnergyBall (LDA.b #$10 : LDX.b #$00) org $1edabf ; <- sprite_energy_ball.asm : 86-7 Sprite_EnergyBall (LDA.b #$10 : LDX.b #$00)
JSL StandardAgaDmg JSL StandardAgaDmg
+59 -17
View File
@@ -7,23 +7,37 @@ DrHudOverride:
HudAdditions: HudAdditions:
{ {
lda.l DRFlags : and #$0008 : beq ++ LDA.l DRFlags : AND #$0008 : BNE + : JMP .end_item_count : +
; LDA.w #$28A4 : STA !GOAL_DRAW_ADDRESS LDA.l $7EF423 : PHA : CMP #1000 : !BLT +
lda $7EF423 JSL HexToDec4Digit_fast
jsr HudHexToDec4DigitCopy LDX.b $04 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS ; draw 1000's digit
LDX.b $05 : TXA : ORA.w #$2400 : STA !GOAL_DRAW_ADDRESS+2 ; draw 100's digit BRA .skip
LDX.b $06 : TXA : ORA.w #$2400 : STA !GOAL_DRAW_ADDRESS+4 ; draw 10's digit + JSL HexToDec_fast
LDX.b $07 : TXA : ORA.w #$2400 : STA !GOAL_DRAW_ADDRESS+6 ; draw 1's digit .skip
LDA #$207F : STA !GOAL_DRAW_ADDRESS+2 : STA !GOAL_DRAW_ADDRESS+4
PLA : PHA : CMP.w #100 : !BLT +
LDX.b $05 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+2 ; draw 100's digit
+ PLA : CMP.w #10 : !BLT +
LDX.b $06 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+4 ; draw 10's digit
+ LDX.b $07 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+6 ; draw 1's digit
LDA.w #$2830 : STA !GOAL_DRAW_ADDRESS+8 ; draw slash LDA.w #$2830 : STA !GOAL_DRAW_ADDRESS+8 ; draw slash
LDA.l DRFlags : AND #$0100 : BNE + LDA.l DRFlags : AND #$0100 : BNE +
lda $7EF33E LDA.l $7EF33E : CMP #1000 : !BLT .three_digit_goal
jsr HudHexToDec4DigitCopy JSL HexToDec4Digit_fast
LDX.b $05 : TXA : ORA.w #$2400 : STA !GOAL_DRAW_ADDRESS+10 ; draw 100's digit LDX.b $04 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+10 ; draw 1000's digit
LDX.b $06 : TXA : ORA.w #$2400 : STA !GOAL_DRAW_ADDRESS+12 ; draw 10's digit LDX.b $05 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+12 ; draw 100's digit
LDX.b $07 : TXA : ORA.w #$2400 : STA !GOAL_DRAW_ADDRESS+14 ; draw 1's digit LDX.b $06 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+14 ; draw 10's digit
BRA ++ LDX.b $07 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+16 ; draw 1's digit
+ LDA.w #$2405 : STA !GOAL_DRAW_ADDRESS+10 : STA !GOAL_DRAW_ADDRESS+12 : STA !GOAL_DRAW_ADDRESS+14 BRA .end_item_count
++ .three_digit_goal
JSL HexToDec_fast
LDX.b $05 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+10 ; draw 100's digit
LDX.b $06 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+12 ; draw 10's digit
LDX.b $07 : TXA : ORA.w #$2490 : STA !GOAL_DRAW_ADDRESS+14 ; draw 1's digit
BRA .end_item_count
+ LDA.w #$2405 : STA !GOAL_DRAW_ADDRESS+10 : STA !GOAL_DRAW_ADDRESS+12
STA !GOAL_DRAW_ADDRESS+14 : STA !GOAL_DRAW_ADDRESS+16
.end_item_count
LDX $1B : BNE + : RTS : + ; Skip if outdoors LDX $1B : BNE + : RTS : + ; Skip if outdoors
ldx $040c : cpx #$ff : bne + : rts : + ; Skip if not in dungeon ldx $040c : cpx #$ff : bne + : rts : + ; Skip if not in dungeon
@@ -141,7 +155,7 @@ DrHudDungeonItemsAdditions:
iny #2 iny #2
lda.w #$24f5 : sta $1644, y ; blank out map spot lda.w #$24f5 : sta $1644, y ; blank out map spot
lda $7ef368 : and.l $0098c0, x : beq + ; must have map lda $7ef368 : and.l $0098c0, x : beq + ; must have map
lda #$2826 : sta $1644, y ; check mark JSR MapIndicatorShort : STA $1644, Y
+ iny #2 + iny #2
cpx #$001a : bne + cpx #$001a : bne +
tya : !add #$003c : tay tya : !add #$003c : tay
@@ -164,6 +178,34 @@ DrHudDungeonItemsAdditions:
plp : ply : plx : rtl plp : ply : plx : rtl
} }
MapIndicatorLong:
PHX
LDA.l OldHudToNewHudTable, X : TAX
JSR MapIndicator
PLX
RTL
MapIndicatorShort:
PHX
TXA : LSR : TAX
JSR MapIndicator
PLX
RTS
OldHudToNewHudTable:
dw 1, 2, 3, 10, 4, 6, 5, 8, 11, 9, 7, 12, 13
IndicatorCharacters:
; check 1 2 3 4 5 6 7 G B R
dw $2426, $2817, $2818, $2819, $281A, $281B, $281C, $281D, $2590, $258B, $259B
MapIndicator:
LDA.l CrystalPendantFlags_3, X : AND #$00FF
PHX
ASL : TAX : LDA.l IndicatorCharacters, X
PLX
RTS
BkStatus: BkStatus:
lda $7ef366 : and.l $0098c0, x : bne +++ ; has the bk already lda $7ef366 : and.l $0098c0, x : bne +++ ; has the bk already
lda.l BigKeyStatus, x : bne ++ lda.l BigKeyStatus, x : bne ++
@@ -181,7 +223,7 @@ ConvertToDisplay:
ConvertToDisplay2: ConvertToDisplay2:
and.w #$00ff : beq ++ and.w #$00ff : beq ++
cmp #$000a : !blt + cmp #$000a : !blt +
!add #$2553 : rts !add #$2553 : rts ; todo: use 2580 with 258A as "A" for non transparent digits
+ !add #$2816 : rts + !add #$2816 : rts
++ lda #$2827 : rts ; 0/O for 0 or placeholder digit ;2483 ++ lda #$2827 : rts ; 0/O for 0 or placeholder digit ;2483
-181
View File
@@ -1,181 +0,0 @@
org $06926e ; <- 3126e - sprite_prep.asm : 2664 (LDA $0B9B : STA $0CBA, X)
jsl SpriteKeyPrep : nop #2
org $06d049 ; <- 35049 sprite_absorbable : 31-32 (JSL Sprite_DrawRippleIfInWater : JSR Sprite_DrawAbsorbable)
jsl SpriteKeyDrawGFX : bra + : nop : +
org $06d180
jsl BigKeyGet : bcs $07 : nop #5
org $06d18d ; <- 3518D - sprite_absorbable.asm : 274 (LDA $7EF36F : INC A : STA $7EF36F)
jsl KeyGet
org $06f9f3 ; bank06.asm : 6732 (JSL Sprite_LoadProperties)
jsl LoadProperties_PreserveItemMaybe
org $06d23a
Sprite_DrawAbsorbable:
org $1eff81
Sprite_DrawRippleIfInWater:
org $0db818
Sprite_LoadProperties:
org $288000 ;140000
ShuffleKeyDrops:
db 0
MultiClientFlags: ; 140001 -> stored in SRAM at 7ef33d
db 0
LootTable: ;PC: 140002
db $0e, $00, $24 ;; ice jelly key
db $13, $00, $24 ;; pokey 2
db $16, $00, $24 ;; swamp waterway pot
db $21, $00, $24 ;; key rat
db $35, $00, $24 ;; swamp trench 2 pot
db $36, $00, $24 ;; hookshot pot
db $37, $00, $24 ;; trench 1 pot
db $38, $00, $24 ;; pot row pot
db $39, $00, $24 ;; skull gibdo
db $3d, $00, $24 ;; gt minihelma
db $3e, $00, $24 ;; ice conveyor
db $3f, $00, $24 ;; ice hammer block ??? is this a dungeon secret?
db $43, $00, $24 ;; tiles 2 pot
db $53, $00, $24 ;; beamos hall pot
db $56, $00, $24 ;; skull west lobby pot
db $63, $00, $24 ;; desert tiles 1 pot
db $71, $00, $24 ;; boomerang guard
db $72, $00, $24 ;; hc map guard
db $7b, $00, $24 ;; gt star pits pot
db $80, $00, $32 ;; a big key (for the current dungeon)
db $8b, $00, $24 ;; gt conv cross block
db $9b, $00, $24 ;; gt dlb switch pot
db $9f, $00, $24 ;; ice many pots
db $99, $00, $24 ;; eastern eyegore
db $a1, $00, $24 ;; mire fishbone pot
db $ab, $00, $24 ;; tt spike switch pot
db $b0, $00, $24 ;; tower circle of pots usain
db $b3, $00, $24 ;; mire spikes pot
db $b6, $00, $24 ;; pokey 1
db $ba, $00, $24 ;; eastern dark pot
db $bc, $00, $24 ;; tt hallway pot
db $c0, $00, $24 ;; tower dark archer
db $c1, $00, $24 ;; mire glitchy jelly
db $ff, $00, $ff
;140068
KeyTable:
db $a0, $a0, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9, $aa, $ab, $ac, $ad
SpriteKeyPrep:
{
lda $0b9b : sta $0cba, x ; what we wrote over
pha
lda.l ShuffleKeyDrops : beq +
phx
ldx #$fd
- inx #3 : lda.l LootTable, x : cmp #$ff : beq ++ : cmp $a0 : bne -
inx : lda.l LootTable, x : sta !MULTIWORLD_SPRITEITEM_PLAYER_ID
inx : lda.l LootTable, x
plx : sta $0e80, x
cmp #$24 : bne +++
lda $a0 : cmp #$80 : bne + : lda #$24
+++ jsl PrepDynamicTile : bra +
++ plx : lda #$24 : sta $0e80, x
+ pla
rtl
}
SpriteKeyDrawGFX:
{
jsl Sprite_DrawRippleIfInWater
pha
lda.l ShuffleKeyDrops : bne +
- pla
phk : pea.w .jslrtsreturn-1
pea.w $068014 ; an rtl address - 1 in Bank06
jml Sprite_DrawAbsorbable
.jslrtsreturn
rtl
+ lda $0e80, x
cmp #$24 : bne +
lda $a0 : cmp #$80 : bne - : lda #$24
+ jsl DrawDynamicTile ; see DrawHeartPieceGFX if problems
cmp #$03 : bne +
pha : lda $0e60, x : ora.b #$20 : sta $0E60, x : pla
+
jsl.l Sprite_DrawShadowLong
pla : rtl
}
KeyGet:
{
lda $7ef36f ; what we wrote over
pha
lda.l ShuffleKeyDrops : bne +
- pla : rtl
+ ldy $0e80, x
lda $a0 : cmp #$87 : bne +
jsr ShouldKeyBeCountedForDungeon : bcc -
jsl CountChestKeyLong : bra -
+ sty $00
jsr KeyGetPlayer : sta !MULTIWORLD_ITEM_PLAYER_ID
lda !MULTIWORLD_ITEM_PLAYER_ID : bne .receive
phx
lda $040c : lsr : tax
lda $00 : CMP.l KeyTable, x : bne +
- JSL.l FullInventoryExternal : jsl CountChestKeyLong : plx : pla : rtl
+ cmp #$af : beq - ; universal key
cmp #$24 : beq - ; small key for this dungeon
plx
.receive
jsl.l $0791b3 ; Player_HaltDashAttackLong
jsl.l Link_ReceiveItem
pla : dec : rtl
}
; Input Y - the item type
ShouldKeyBeCountedForDungeon:
{
phx
lda $040c : lsr : tax
tya : cmp KeyTable, x : bne +
- plx : sec : rts
+ cmp #$24 : beq -
plx : clc : rts
}
BigKeyGet:
{
lda.l ShuffleKeyDrops : bne +
- stz $02e9 : ldy.b #$32 ; what we wrote over
phx : jsl Link_ReceiveItem : plx ; what we wrote over
clc : rtl
+
ldy $0e80, x
cpy #$32 : beq -
+ sec : rtl
}
KeyGetPlayer:
{
phx
ldx #$fd
- inx #3 : lda.l LootTable, x : cmp #$ff : beq ++ : cmp $a0 : bne -
++ inx : lda.l LootTable, x
plx
rts
}
LoadProperties_PreserveItemMaybe:
{
lda.l ShuffleKeyDrops : bne +
JML Sprite_LoadProperties
+ lda $0e80, x : pha
jsl Sprite_LoadProperties
pla : sta $0e80, x
rtl
}
+6 -4
View File
@@ -118,7 +118,7 @@ RetrieveBunnyState:
+ RTL + RTL
RainPrevention: RainPrevention:
LDA $00 : XBA : AND #$00FF ; what we wrote over LDA $00 : XBA : AND #$00FF : STA.b $0A ; what we wrote over
PHA PHA
LDA $7EF3C5 : AND #$00FF : CMP #$0002 : !BGE .done ; only in rain states (0 or 1) LDA $7EF3C5 : AND #$00FF : CMP #$0002 : !BGE .done ; only in rain states (0 or 1)
LDA.l $7EF3C6 : AND #$0004 : BNE .done ; zelda's been rescued LDA.l $7EF3C6 : AND #$0004 : BNE .done ; zelda's been rescued
@@ -131,9 +131,11 @@ RainPrevention:
LDX #$FFFE LDX #$FFFE
- INX #2 : LDA.l RemoveRainDoorsRoom, X : CMP #$FFFF : BEQ .done - INX #2 : LDA.l RemoveRainDoorsRoom, X : CMP #$FFFF : BEQ .done
CMP $A0 : BNE - CMP $A0 : BNE -
LDA.l RainDoorMatch, X : CMP $00 : BNE - SEP #$20 : LDA.l RainDoorMatch, X : CMP $00 : BNE .continue
PLA : LDA #$0008 : RTL REP #$20 : PLA : SEC : RTL
.done PLA : RTL .continue
REP #$20 : BRA -
.done PLA : CLC : RTL
; A should be how much dmg to do to Aga when leaving this function ; A should be how much dmg to do to Aga when leaving this function
StandardAgaDmg: StandardAgaDmg:
+18 -18
View File
@@ -218,14 +218,14 @@ OWMirrorSpriteOnMap:
OWPreserveMirrorSprite: OWPreserveMirrorSprite:
{ {
lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .vanilla ; if OW Crossed, skip world check and continue lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .vanilla ; if OW Crossed, skip world check and continue
lda $10 : cmp.b #$0f : beq .vanilla lda.b $10 : cmp.b #$0f : beq .vanilla ; if performing mirror superbunny
rtl rtl
.vanilla .vanilla
lda.l InvertedMode : beq + lda.l InvertedMode : beq +
lda.l $7ef3ca : beq .deleteMirror lda.l CurrentWorld : beq .deleteMirror
rtl rtl
+ lda.l $7ef3ca : bne .deleteMirror + lda.l CurrentWorld : bne .deleteMirror
rtl rtl
.deleteMirror .deleteMirror
@@ -242,7 +242,7 @@ OWMirrorSpriteMove:
OWMirrorSpriteBonk: OWMirrorSpriteBonk:
{ {
jsr.w OWMirrorSpriteMove jsr.w OWMirrorSpriteMove
lda.b #$2c : jml.l SetGameModeLikeMirror ; what we wrote over lda.b #$2c : jml SetGameModeLikeMirror ; what we wrote over
} }
OWMirrorSpriteDelete: OWMirrorSpriteDelete:
{ {
@@ -254,9 +254,9 @@ OWMirrorSpriteRestore:
{ {
lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .return lda.l OWMode+1 : and.b #!FLAG_OW_CROSSED : beq .return
lda.l InvertedMode : beq + lda.l InvertedMode : beq +
lda.l $7ef3ca : beq .return lda.l CurrentWorld : beq .return
bra .restorePortal bra .restorePortal
+ lda.l $7ef3ca : bne .return + lda.l CurrentWorld : bne .return
.restorePortal .restorePortal
lda.w $1acf : and.b #$0f : sta.w $1acf lda.w $1acf : and.b #$0f : sta.w $1acf
@@ -275,15 +275,15 @@ OWLightWorldOrCrossed:
OWFluteCancel: OWFluteCancel:
{ {
lda.l OWFlags+1 : and #$01 : bne + lda.l OWFlags+1 : and #$01 : bne +
jsl $02e99d : rtl jsl FluteMenu_LoadTransport : rtl
+ lda $7f5006 : cmp #$01 : beq + + lda $7f5006 : cmp #$01 : beq +
jsl $02e99d jsl FluteMenu_LoadTransport
+ lda #$00 : sta $7f5006 : rtl + lda #$00 : sta $7f5006 : rtl
} }
OWFluteCancel2: OWFluteCancel2:
{ {
lda $f2 : ora $f0 : and #$c0 : bne + lda $f2 : ora $f0 : and #$c0 : bne +
jml $0ab7bd jml FluteMenu_HandleSelection_NoSelection
+ inc $0200 + inc $0200
lda.l OWFlags+1 : and #$01 : beq + lda.l OWFlags+1 : and #$01 : beq +
lda $f2 : cmp #$40 : bne + lda $f2 : cmp #$40 : bne +
@@ -292,7 +292,7 @@ OWFluteCancel2:
} }
OWSmithAccept: OWSmithAccept:
{ {
lda $7ef3cc : cmp #$07 : beq + lda FollowerIndicator : cmp #$07 : beq +
cmp #$08 : beq + cmp #$08 : beq +
clc : rtl clc : rtl
+ sec : rtl + sec : rtl
@@ -395,7 +395,7 @@ OWDetectSpecialTransition:
LDA.l OWEdgeDataOffset,X : STA.w $06F8 LDA.l OWEdgeDataOffset,X : STA.w $06F8
PLA : SEP #$30 : PLA ; delete 3 bytes from stack PLA : SEP #$30 : PLA ; delete 3 bytes from stack
JSL Link_CheckForEdgeScreenTransition : BCS .return ; Link_CheckForEdgeScreenTransition JSL Link_CheckForEdgeScreenTransition : BCS .return ; Link_CheckForEdgeScreenTransition
LDA.l $04E879,X : STA.b $00 : CMP.b #$08 : BNE .hobo LDA.l Overworld_CheckForSpecialOverworldTrigger_Direction,X : STA.b $00 : CMP.b #$08 : BNE .hobo
LSR : STA.b $20 : STZ.b $E8 ; move Link and camera to edge LSR : STA.b $20 : STZ.b $E8 ; move Link and camera to edge
LDA.b #$06 : STA.b $02 LDA.b #$06 : STA.b $02
STZ.w $0418 STZ.w $0418
@@ -437,7 +437,7 @@ OWEdgeTransition:
SEP #$30 SEP #$30
RTL RTL
.normal .normal
LDA.l $02A4E3,X : ORA.l $7EF3CA ; what we wrote over LDA.l Overworld_ActualScreenID,X : ORA.l CurrentWorld ; what we wrote over
RTL RTL
} }
OWSpecialExit: OWSpecialExit:
@@ -628,8 +628,8 @@ OWLoadSpecialArea:
} }
OWWorldUpdate: ; x = owid of destination screen OWWorldUpdate: ; x = owid of destination screen
{ {
lda.l OWTileWorldAssoc,x : cmp.l $7ef3ca : beq .return lda.l OWTileWorldAssoc,x : cmp.l CurrentWorld : beq .return
sta.l $7ef3ca ; change world sta.l CurrentWorld ; change world
; moving mirror portal off screen when in DW ; moving mirror portal off screen when in DW
cmp #0 : beq + : lda #1 cmp #0 : beq + : lda #1
@@ -641,10 +641,10 @@ OWWorldUpdate: ; x = owid of destination screen
lda #$38 : sta $012f ; play sfx - #$3b is an alternative lda #$38 : sta $012f ; play sfx - #$3b is an alternative
; toggle bunny mode ; toggle bunny mode
lda $7ef357 : bne .nobunny lda MoonPearlEquipment : bne .nobunny
lda.l InvertedMode : bne .inverted lda.l InvertedMode : bne .inverted
lda $7ef3ca : and.b #$40 : bra + lda CurrentWorld : and.b #$40 : bra +
.inverted lda $7ef3ca : and.b #$40 : eor #$40 .inverted lda CurrentWorld : and.b #$40 : eor #$40
+ cmp #$40 : bne .nobunny + cmp #$40 : bne .nobunny
; turn into bunny ; turn into bunny
lda $5d : cmp #$04 : beq + ; if swimming, continue lda $5d : cmp #$04 : beq + ; if swimming, continue
@@ -681,7 +681,7 @@ OWEndScrollTransition:
CMP.w $06FC CMP.w $06FC
RTL RTL
.normal .normal
CMP.l $02C176,X ; what we wrote over CMP.l Overworld_FinalizeEntryOntoScreen_Data,X ; what we wrote over
RTL RTL
} }
Binary file not shown.
+16 -10
View File
@@ -30,9 +30,19 @@
1: 2 1: 2
2: 2 2: 2
3: 4 3: 4
keydropshuffle: dropshuffle:
on: 1 on: 1
off: 1 off: 1
pottery:
none: 8
keys: 1
cave: 1
dungeon: 1
lottery: 1
cavekeys: 1
reduced: 1
clustered: 1
nonempty: 1
shopsanity: shopsanity:
on: 1 on: 1
off: 1 off: 1
@@ -81,17 +91,17 @@
dungeons: 1 dungeons: 1
pedestal: 2 pedestal: 2
triforce-hunt: 2 triforce-hunt: 2
trinity: 1 trinity: 2
triforce_goal_min: 10 triforce_goal_min: 10
triforce_goal_max: 30 triforce_goal_max: 30
triforce_pool_min: 20 triforce_pool_min: 20
triforce_pool_max: 40 triforce_pool_max: 40
triforce_min_difference: 10 triforce_min_difference: 10
algorithm: algorithm:
balanced: 12
vanilla_fill: 1
major_only: 1 major_only: 1
dungeon_only: 1 dungeon_only: 1
vanilla_fill: 1
balanced: 10
district: 1 district: 1
restrict_boss_items: restrict_boss_items:
none: 1 none: 1
@@ -175,13 +185,9 @@
on: 1 on: 1
off: 0 off: 0
startinventory: startinventory:
"Pegasus Boots": Pegasus Boots: on
on: 0
off: 1
rom: rom:
quickswap: quickswap: on
on: 1
off: 0
heartcolor: heartcolor:
red: 1 red: 1
blue: 1 blue: 1
+15 -5
View File
@@ -1,5 +1,4 @@
description: A test suite for testing various combinations description: A test suite for testing various combinations
# Not yet in this branch
algorithm: algorithm:
major_only: 1 major_only: 1
dungeon_only: 1 dungeon_only: 1
@@ -13,10 +12,19 @@ door_shuffle:
intensity: intensity:
1: 1 1: 1
2: 1 2: 1
3: 2 # intensity 3 usuall yield more errors 3: 2 # intensity 3 usually yield more errors
keydropshuffle: dropshuffle:
on: 1 on: 1
off: 1 off: 1
pottery:
none: 10 # fewer locations
keys: 1
cave: 1
cavekeys: 1
dungeon: 1
reduced: 1
clustered: 1
lottery: 1
shopsanity: shopsanity:
on: 1 on: 1
off: 1 off: 1
@@ -46,9 +54,10 @@ retro:
goals: goals:
ganon: 1 ganon: 1
fast_ganon: 1 fast_ganon: 1
dungeons: 2 # this yields more errors so is preferred dungeons: 3 # this yields more errors so is preferred
pedestal: 1 pedestal: 1
triforce-hunt: 1 triforce-hunt: 1
trinity: 1
triforce_goal_min: 20 triforce_goal_min: 20
triforce_goal_max: 30 triforce_goal_max: 30
triforce_pool_min: 30 triforce_pool_min: 30
@@ -83,7 +92,7 @@ accessibility:
none: 0 # i'm not really interested in this yet none: 0 # i'm not really interested in this yet
restrict_boss_items: restrict_boss_items:
none: 1 none: 1
mapcompass: 1 # mapcompass: 1 has confirmed issues
dungeon: 1 dungeon: 1
tower_open: tower_open:
"0": 1 "0": 1
@@ -108,6 +117,7 @@ ganon_open:
boss_shuffle: boss_shuffle:
none: 1 none: 1
simple: 1 simple: 1
unique: 1
full: 1 full: 1
random: 1 random: 1
enemy_shuffle: # shouldn't affect generation enemy_shuffle: # shouldn't affect generation
+49 -8
View File
@@ -13,6 +13,11 @@
"suppress_spoiler": { "suppress_spoiler": {
"action": "store_true" "action": "store_true"
}, },
"mystery": {
"action": "store_true",
"type": "bool",
"help": "suppress"
},
"logic": { "logic": {
"choices": [ "choices": [
"noglitches", "noglitches",
@@ -43,8 +48,8 @@
"pedestal", "pedestal",
"dungeons", "dungeons",
"triforcehunt", "triforcehunt",
"crystals", "trinity",
"trinity" "crystals"
] ]
}, },
"difficulty": { "difficulty": {
@@ -69,7 +74,32 @@
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
}, },
"mixed_travel": { "dropshuffle" : {
"action": "store_true",
"type": "bool"
},
"pottery" : {
"choices" : [
"none",
"keys",
"dungeon",
"cave",
"cavekeys",
"reduced",
"clustered",
"nonempty",
"lottery"
]
},
"colorizepots" : {
"action": "store_true",
"type": "bool"
},
"shufflepots": {
"action": "store_true",
"type": "bool"
},
"mixed_travel" : {
"choices": [ "choices": [
"prevent", "prevent",
"allow", "allow",
@@ -102,7 +132,6 @@
"algorithm": { "algorithm": {
"choices": [ "choices": [
"balanced", "balanced",
"equitable",
"vanilla_fill", "vanilla_fill",
"major_only", "major_only",
"dungeon_only", "dungeon_only",
@@ -259,6 +288,14 @@
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
}, },
"msu_resume": {
"action": "store_true",
"type": "bool"
},
"collection_rate": {
"action": "store_true",
"type": "bool"
},
"mapshuffle": { "mapshuffle": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
@@ -368,6 +405,10 @@
"dest": "create_rom", "dest": "create_rom",
"help": "suppress" "help": "suppress"
}, },
"suppress_meta": {
"action": "store_true",
"type": "bool"
},
"shuffleganon": { "shuffleganon": {
"action": "store_true", "action": "store_true",
"type": "bool", "type": "bool",
@@ -407,6 +448,9 @@
"jsonout": { "jsonout": {
"action": "store_true" "action": "store_true"
}, },
"bps": {
"action": "store_true"
},
"enemizercli": { "enemizercli": {
"setting": "enemizercli" "setting": "enemizercli"
}, },
@@ -414,6 +458,7 @@
"choices": [ "choices": [
"none", "none",
"simple", "simple",
"unique",
"full", "full",
"random" "random"
] ]
@@ -442,10 +487,6 @@
"random" "random"
] ]
}, },
"shufflepots": {
"action": "store_true",
"type": "bool"
},
"remote_items": { "remote_items": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
+19 -3
View File
@@ -39,6 +39,7 @@
"cannot.reach.required": "Not all required items reachable. Something went terribly wrong here.", "cannot.reach.required": "Not all required items reachable. Something went terribly wrong here.",
"patching.rom": "Patching ROM", "patching.rom": "Patching ROM",
"patching.spoiler": "Creating Spoiler", "patching.spoiler": "Creating Spoiler",
"create.meta": "Creating Meta Info",
"calc.playthrough": "Calculating Playthrough", "calc.playthrough": "Calculating Playthrough",
"made.rom": "Patched ROM: %s", "made.rom": "Patched ROM: %s",
"made.playthrough": "Printed Playthrough: %s", "made.playthrough": "Printed Playthrough: %s",
@@ -60,6 +61,7 @@
"help": { "help": {
"lang": [ "App Language, if available, defaults to English" ], "lang": [ "App Language, if available, defaults to English" ],
"create_spoiler": [ "Output a Spoiler File" ], "create_spoiler": [ "Output a Spoiler File" ],
"bps": [ "Output BPS patches instead of ROMs"],
"logic": [ "logic": [
"Select Enforcement of Item Requirements. (default: %(default)s)", "Select Enforcement of Item Requirements. (default: %(default)s)",
"No Glitches: No Glitch knowledge required.", "No Glitches: No Glitch knowledge required.",
@@ -157,8 +159,6 @@
"balanced: vt26 derivative that aims to strike a balance between", "balanced: vt26 derivative that aims to strike a balance between",
" the overworld heavy vt25 and the dungeon heavy vt26", " the overworld heavy vt25 and the dungeon heavy vt26",
" algorithm.", " algorithm.",
"equitable: does not place dungeon items first allowing new potential",
" but mixed with the normal advancement pool",
"restricted placements: these consider all major items to be special and attempts", "restricted placements: these consider all major items to be special and attempts",
"to place items from fixed to semi-random locations. For purposes of these shuffles, all", "to place items from fixed to semi-random locations. For purposes of these shuffles, all",
"Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and", "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and",
@@ -295,7 +295,21 @@
"keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"shopsanity": ["Shop contents are shuffle in the main item pool and other items can take their place. (default: %(default)s)"], "shopsanity": ["Shop contents are shuffle in the main item pool and other items can take their place. (default: %(default)s)"],
"keydropshuffle": [ "Key Drops (Pots and Enemies) are shuffled and other items can take their place (default: %(default)s)" ], "dropshuffle": [ "Keys dropped by enemies are shuffled and other items can take their place. (default: %(default)s)"],
"pottery": [ "Controls how items under pots are shuffled and if other items can take their place:",
"None: No pots are changed",
"Keys: Key pots are included in the location pool and other items can take their place",
"Cave: Only pots in houses and caves are included in the location pool",
"CaveKeys: Both pots in houses and caves and keys pots are included in the location pool",
"Reduced: Same as KeyCaves + 25%% of Pots in dungeons (dynamic mode)",
"Clustered: Same as KeyCaves + 50%% of Pots in dungeons, chosen by logical group (dynamic mode)",
"NonEmpty: All pots that are not originally empty are included in the location pool",
"Dungeon: Only pots in dungeons are included in the location pool",
"Lottery: All pots are part of the location pool"
],
"colorizepots": ["All pots chosen to be in location pool by the pottery setting are different.",
"Forced on in dynamic modes. Forced off in lottery"],
"shufflepots": [ "Pots and switches are shuffled on the supertile (legacy potshuffle) (default: %(default)s)"],
"mixed_travel": [ "mixed_travel": [
"How to handle potential traversal between dungeon in Crossed door shuffle", "How to handle potential traversal between dungeon in Crossed door shuffle",
"Prevent: Rails are placed to prevent bombs jump and hovering from changing dungeon except with glitched logic settings", "Prevent: Rails are placed to prevent bombs jump and hovering from changing dungeon except with glitched logic settings",
@@ -356,6 +370,8 @@
], ],
"reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ], "reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ],
"shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ], "shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ],
"msu_resume": [ "Enable MSU Resume (default: %(default)s)" ],
"collection_rate": [ "Display collection rate (default: %(default)s)" ],
"create_rom": [ "Create an output rom file. (default: %(default)s)" ], "create_rom": [ "Create an output rom file. (default: %(default)s)" ],
"gui": [ "Launch the GUI. (default: %(default)s)" ], "gui": [ "Launch the GUI. (default: %(default)s)" ],
"jsonout": [ "jsonout": [
@@ -1,6 +1,7 @@
{ {
"checkboxes": { "checkboxes": {
"nobgm": { "type": "checkbox" }, "nobgm": { "type": "checkbox" },
"msu_resume": { "type": "checkbox" },
"quickswap": { "type": "checkbox" }, "quickswap": { "type": "checkbox" },
"reduce_flashing": {"type": "checkbox" }, "reduce_flashing": {"type": "checkbox" },
"shuffle_sfx": {"type": "checkbox" } "shuffle_sfx": {"type": "checkbox" }
+19 -3
View File
@@ -4,6 +4,7 @@
"adjust.quickswap": "L/R Quickswapping", "adjust.quickswap": "L/R Quickswapping",
"adjust.reduce_flashing": "Reduce Flashing", "adjust.reduce_flashing": "Reduce Flashing",
"adjust.shuffle_sfx": "Shuffle Sound Effects", "adjust.shuffle_sfx": "Shuffle Sound Effects",
"adjust.msu_resume": "MSU Resume",
"adjust.heartcolor": "Heart Color", "adjust.heartcolor": "Heart Color",
"adjust.heartcolor.red": "Red", "adjust.heartcolor.red": "Red",
@@ -53,7 +54,20 @@
"randomizer.dungeon.compassshuffle": "Compasses", "randomizer.dungeon.compassshuffle": "Compasses",
"randomizer.dungeon.smallkeyshuffle": "Small Keys", "randomizer.dungeon.smallkeyshuffle": "Small Keys",
"randomizer.dungeon.bigkeyshuffle": "Big Keys", "randomizer.dungeon.bigkeyshuffle": "Big Keys",
"randomizer.dungeon.keydropshuffle": "Key Drops (pots and enemies)", "randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)",
"randomizer.dungeon.dropshuffle": "Shuffle Enemy Key Drops",
"randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)",
"randomizer.dungeon.pottery": "Pottery",
"randomizer.dungeon.pottery.none": "None",
"randomizer.dungeon.pottery.keys": "Key Pots",
"randomizer.dungeon.pottery.cave": "Cave Pots",
"randomizer.dungeon.pottery.cavekeys": "Cave+Key Pots",
"randomizer.dungeon.pottery.reduced": "Reduced Dungeon Pots (Dynamic)",
"randomizer.dungeon.pottery.clustered": "Clustered Dungeon Pots (Dynamic)",
"randomizer.dungeon.pottery.nonempty": "Excludes Empty Pots",
"randomizer.dungeon.pottery.dungeon": "Dungeon Pots",
"randomizer.dungeon.pottery.lottery": "Lottery (All Pots and Large Blocks)",
"randomizer.dungeon.colorizepots": "Colorize Randomized Pots",
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle", "randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla", "randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
@@ -66,7 +80,6 @@
"randomizer.dungeon.dungeonintensity.3": "3: Dungeon Lobbies", "randomizer.dungeon.dungeonintensity.3": "3: Dungeon Lobbies",
"randomizer.dungeon.dungeonintensity.random": "Random", "randomizer.dungeon.dungeonintensity.random": "Random",
"randomizer.dungeon.potshuffle": "Pot Shuffle",
"randomizer.dungeon.experimental": "Enable Experimental Features", "randomizer.dungeon.experimental": "Enable Experimental Features",
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters", "randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
@@ -93,6 +106,7 @@
"randomizer.enemizer.bossshuffle": "Boss Shuffle", "randomizer.enemizer.bossshuffle": "Boss Shuffle",
"randomizer.enemizer.bossshuffle.none": "None", "randomizer.enemizer.bossshuffle.none": "None",
"randomizer.enemizer.bossshuffle.simple": "Simple", "randomizer.enemizer.bossshuffle.simple": "Simple",
"randomizer.enemizer.bossshuffle.unique": "Unique",
"randomizer.enemizer.bossshuffle.full": "Full", "randomizer.enemizer.bossshuffle.full": "Full",
"randomizer.enemizer.bossshuffle.random": "Random", "randomizer.enemizer.bossshuffle.random": "Random",
@@ -170,6 +184,8 @@
"randomizer.gameoptions.quickswap": "L/R Quickswapping", "randomizer.gameoptions.quickswap": "L/R Quickswapping",
"randomizer.gameoptions.reduce_flashing": "Reduce Flashing", "randomizer.gameoptions.reduce_flashing": "Reduce Flashing",
"randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects", "randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects",
"randomizer.gameoptions.msu_resume": "MSU Resume",
"randomizer.gameoptions.collection_rate": "Display Collection Rate",
"randomizer.gameoptions.heartcolor": "Heart Color", "randomizer.gameoptions.heartcolor": "Heart Color",
"randomizer.gameoptions.heartcolor.red": "Red", "randomizer.gameoptions.heartcolor.red": "Red",
@@ -207,6 +223,7 @@
"randomizer.gameoptions.sprite.unchanged": "(unchanged)", "randomizer.gameoptions.sprite.unchanged": "(unchanged)",
"randomizer.generation.bps": "Create BPS Patches",
"randomizer.generation.createspoiler": "Create Spoiler Log", "randomizer.generation.createspoiler": "Create Spoiler Log",
"randomizer.generation.createrom": "Create Patched ROM", "randomizer.generation.createrom": "Create Patched ROM",
"randomizer.generation.calcplaythrough": "Calculate Playthrough", "randomizer.generation.calcplaythrough": "Calculate Playthrough",
@@ -315,7 +332,6 @@
"randomizer.item.sortingalgo": "Item Sorting", "randomizer.item.sortingalgo": "Item Sorting",
"randomizer.item.sortingalgo.balanced": "Balanced", "randomizer.item.sortingalgo.balanced": "Balanced",
"randomizer.item.sortingalgo.equitable": "Equitable",
"randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill", "randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill",
"randomizer.item.sortingalgo.major_only": "Major Location Restriction", "randomizer.item.sortingalgo.major_only": "Major Location Restriction",
"randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction", "randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction",
@@ -3,7 +3,6 @@
"mapshuffle": { "type": "checkbox" }, "mapshuffle": { "type": "checkbox" },
"compassshuffle": { "type": "checkbox" }, "compassshuffle": { "type": "checkbox" },
"smallkeyshuffle": { "type": "checkbox" }, "smallkeyshuffle": { "type": "checkbox" },
"bigkeyshuffle": { "type": "checkbox" }, "bigkeyshuffle": { "type": "checkbox" }
"keydropshuffle": { "type": "checkbox" }
} }
} }
@@ -22,6 +22,27 @@
"width": 45 "width": 45
} }
}, },
"keydropshuffle": { "type": "checkbox" },
"pottery": {
"type": "selectbox",
"default": "none",
"options": [
"none",
"keys",
"cave",
"cavekeys",
"reduced",
"clustered",
"nonempty",
"dungeon",
"lottery"
],
"config": {
"width": 35
}
},
"colorizepots": { "type": "checkbox" },
"dropshuffle": { "type": "checkbox" },
"potshuffle": { "type": "checkbox" }, "potshuffle": { "type": "checkbox" },
"experimental": { "type": "checkbox" }, "experimental": { "type": "checkbox" },
"dungeon_counters": { "dungeon_counters": {
@@ -15,6 +15,7 @@
"none", "none",
"simple", "simple",
"full", "full",
"unique",
"random" "random"
] ]
} }
@@ -1,6 +1,8 @@
{ {
"checkboxes": { "checkboxes": {
"nobgm": { "type": "checkbox" }, "nobgm": { "type": "checkbox" },
"msu_resume": { "type": "checkbox" },
"collection_rate": {"type": "checkbox"},
"quickswap": { "type": "checkbox" }, "quickswap": { "type": "checkbox" },
"reduce_flashing": { "type": "checkbox" }, "reduce_flashing": { "type": "checkbox" },
"shuffle_sfx": { "type": "checkbox" } "shuffle_sfx": { "type": "checkbox" }
@@ -1,7 +1,8 @@
{ {
"checkboxes": { "checkboxes": {
"createspoiler": { "type": "checkbox" },
"createrom": { "type": "checkbox" }, "createrom": { "type": "checkbox" },
"bps": { "type": "checkbox" },
"createspoiler": { "type": "checkbox" },
"calcplaythrough": { "type": "checkbox" }, "calcplaythrough": { "type": "checkbox" },
"usestartinventory": { "type": "checkbox" }, "usestartinventory": { "type": "checkbox" },
"usecustompool": { "type": "checkbox" } "usecustompool": { "type": "checkbox" }
@@ -118,7 +118,6 @@
"default": "balanced", "default": "balanced",
"options": [ "options": [
"balanced", "balanced",
"equitable",
"vanilla_fill", "vanilla_fill",
"major_only", "major_only",
"dungeon_only", "dungeon_only",
+7 -10
View File
@@ -1,5 +1,5 @@
import random import random
from Utils import int16_as_bytes from Utils import int16_as_bytes, snes_to_pc
class SFX(object): class SFX(object):
@@ -153,16 +153,13 @@ def shuffle_sfx_data():
sfx_table = { sfx_table = {
2: 0x1a8c29, 2: 0x1A8BD0,
3: 0x1A8D25 3: 0x1A8CCC
} }
# 0x1a8c29
# d8059
sfx_accompaniment_table = { sfx_accompaniment_table = {
2: 0x1A8CA7, 2: 0x1A8C4E,
3: 0x1A8DA3 3: 0x1A8D4A
} }
@@ -171,9 +168,9 @@ def randomize_sfx(rom):
for shuffled_sfx in sfx_map.values(): for shuffled_sfx in sfx_map.values():
for sfx in shuffled_sfx.values(): for sfx in shuffled_sfx.values():
base_address = sfx_table[sfx.target_set] base_address = snes_to_pc(sfx_table[sfx.target_set])
rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr)) rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr))
ac_base = sfx_accompaniment_table[sfx.target_set] ac_base = snes_to_pc(sfx_accompaniment_table[sfx.target_set])
last = sfx.target_id last = sfx.target_id
if sfx.target_chain: if sfx.target_chain:
for chained in sfx.target_chain: for chained in sfx.target_chain:
+8 -2
View File
@@ -101,9 +101,12 @@ SETTINGSTOPROCESS = {
"compassshuffle": "compassshuffle", "compassshuffle": "compassshuffle",
"smallkeyshuffle": "keyshuffle", "smallkeyshuffle": "keyshuffle",
"bigkeyshuffle": "bigkeyshuffle", "bigkeyshuffle": "bigkeyshuffle",
"keydropshuffle": "keydropshuffle",
"dungeondoorshuffle": "door_shuffle", "dungeondoorshuffle": "door_shuffle",
"dungeonintensity": "intensity", "dungeonintensity": "intensity",
"keydropshuffle": "keydropshuffle",
"dropshuffle": "dropshuffle",
"pottery": "pottery",
"colorizepots": "colorizepots",
"potshuffle": "shufflepots", "potshuffle": "shufflepots",
"experimental": "experimental", "experimental": "experimental",
"dungeon_counters": "dungeon_counters", "dungeon_counters": "dungeon_counters",
@@ -119,9 +122,12 @@ SETTINGSTOPROCESS = {
"owpalettes": "ow_palettes", "owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes", "uwpalettes": "uw_palettes",
"reduce_flashing": "reduce_flashing", "reduce_flashing": "reduce_flashing",
"shuffle_sfx": "shuffle_sfx" "shuffle_sfx": "shuffle_sfx",
'msu_resume': 'msu_resume',
'collection_rate': 'collection_rate',
}, },
"generation": { "generation": {
"bps": "bps",
"createspoiler": "create_spoiler", "createspoiler": "create_spoiler",
"createrom": "create_rom", "createrom": "create_rom",
"calcplaythrough": "calc_playthrough", "calcplaythrough": "calc_playthrough",
+58
View File
@@ -0,0 +1,58 @@
from RoomData import DoorKind, Position
from source.dungeon.RoomObject import RoomObject, DoorObject
class Room:
def __init__(self, layout, layer1, layer2, doors):
self.layout = layout
self.layer1 = layer1
self.layer2 = layer2
self.doors = doors
def write_to_rom(self, address, rom):
rom.write_bytes(address, self.layout)
address += 2
for obj in self.layer1:
rom.write_bytes(address, obj.data)
address += 3
rom.write_bytes(address, [0xFF, 0xFF])
address += 2
for obj in self.layer2:
rom.write_bytes(address, obj.data)
address += 3
rom.write_bytes(address, [0xFF, 0xFF, 0xF0, 0xFF])
address += 4
for door in self.doors:
rom.write_bytes(address, door.get_bytes())
address += 2
rom.write_bytes(address, [0xFF, 0xFF])
return address + 2 # where the data ended
Room0127 = Room([0xE1, 0x00],
[RoomObject(0x0AB600, [0xFE, 0x89, 0x00]),
RoomObject(0x0AB603, [0xA2, 0xA1, 0x61]),
RoomObject(0x0AB606, [0xFE, 0x8E, 0x81]),
RoomObject(0x0AB609, [0xFF, 0x49, 0x02]),
RoomObject(0x0AB60C, [0xD2, 0xA1, 0x62]),
RoomObject(0x0AB60F, [0xFF, 0x4E, 0x83]),
RoomObject(0x0AB612, [0x20, 0xB3, 0xDD]),
RoomObject(0x0AB615, [0x50, 0xB3, 0xDD]),
RoomObject(0x0AB618, [0x33, 0xCB, 0xFA]),
RoomObject(0x0AB61B, [0x3B, 0xCB, 0xFA]),
RoomObject(0x0AB61E, [0x43, 0xCB, 0xFA]),
RoomObject(0x0AB621, [0x4B, 0xCB, 0xFA]),
RoomObject(0x0AB624, [0xBF, 0x94, 0xF9]),
RoomObject(0x0AB627, [0xB3, 0xB3, 0xFA]),
RoomObject(0x0AB62A, [0xCB, 0xB3, 0xFA]),
RoomObject(0x0AB62D, [0xAD, 0xC8, 0xDF]),
RoomObject(0x0AB630, [0xC4, 0xC8, 0xDF]),
RoomObject(0x0AB633, [0xB3, 0xE3, 0xFA]),
RoomObject(0x0AB636, [0xCB, 0xE3, 0xFA]),
RoomObject(0x0AB639, [0x81, 0x93, 0xC0]),
RoomObject(0x0AB63C, [0x81, 0xD2, 0xC0]),
RoomObject(0x0AB63F, [0xE1, 0x93, 0xC0]),
RoomObject(0x0AB642, [0xE1, 0xD2, 0xC0])],
[], [DoorObject(Position.SouthW, DoorKind.CaveEntrance),
DoorObject(Position.SouthE, DoorKind.CaveEntrance)])
+34
View File
@@ -0,0 +1,34 @@
from Utils import snes_to_pc
# Subtype 3 object (0x2xx by jpdasm id - see bank 01)
# B
Normal_Pot = (0xFA, 3, 3)
Shuffled_Pot = (0xFB, 0, 0) # formerly weird pot, or black diagonal thing
class RoomObject:
def __init__(self, address, data):
self.address = address
self.data = data
def change_type(self, new_type):
type_id, datum_a, datum_b = new_type
if 0xF8 <= type_id < 0xFC: # sub type 3
self.data = (self.data[0] & 0xFC) | datum_a, (self.data[1] & 0xFC) | datum_b, type_id
else:
pass # not yet implemented
def write_to_rom(self, rom):
rom.write_bytes(snes_to_pc(self.address), self.data)
class DoorObject:
def __init__(self, pos, kind):
self.pos = pos
self.kind = kind
def get_bytes(self):
return [self.pos.value, self.kind.value]
+59 -4
View File
@@ -1,5 +1,5 @@
from tkinter import ttk, filedialog, messagebox, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT, X, BOTTOM from tkinter import ttk, filedialog, messagebox, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT, X, BOTTOM
from AdjusterMain import adjust from AdjusterMain import adjust, patch
from argparse import Namespace from argparse import Namespace
from source.classes.SpriteSelector import SpriteSelector from source.classes.SpriteSelector import SpriteSelector
import source.gui.widgets as widgets import source.gui.widgets as widgets
@@ -79,7 +79,9 @@ def adjust_page(top, parent, settings):
romEntry2 = Entry(adjustRomFrame, textvariable=self.romVar2) romEntry2 = Entry(adjustRomFrame, textvariable=self.romVar2)
def RomSelect2(): def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")]) initdir = os.path.join(os.getcwd(), settings['outputpath']) if 'outputpath' in settings else os.getcwd()
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")],
initialdir=initdir)
if rom: if rom:
settings["rom"] = rom settings["rom"] = rom
self.romVar2.set(rom) self.romVar2.set(rom)
@@ -103,7 +105,8 @@ def adjust_page(top, parent, settings):
"quickswap": "quickswap", "quickswap": "quickswap",
"nobgm": "disablemusic", "nobgm": "disablemusic",
"reduce_flashing": "reduce_flashing", "reduce_flashing": "reduce_flashing",
"shuffle_sfx": "shuffle_sfx" 'msu_resume': 'msu_resume',
"shuffle_sfx": "shuffle_sfx",
} }
guiargs = Namespace() guiargs = Namespace()
for option in options: for option in options:
@@ -122,6 +125,58 @@ def adjust_page(top, parent, settings):
messagebox.showinfo(title="Success", message="Rom patched successfully") messagebox.showinfo(title="Success", message="Rom patched successfully")
adjustButton = Button(self.frames["bottomAdjustFrame"], text='Adjust Rom', command=adjustRom) adjustButton = Button(self.frames["bottomAdjustFrame"], text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=BOTTOM, padx=(5, 0)) adjustButton.pack(padx=(5, 0))
patchFileFrame = Frame(self.frames["bottomAdjustFrame"])
patchFileLabel = Label(patchFileFrame, text='BPS Patch: ')
self.patchVar = StringVar(value=settings["patch"])
patchEntry = Entry(patchFileFrame, textvariable=self.patchVar)
def PatchSelect():
initdir = os.path.join(os.getcwd(), settings['outputpath']) if 'outputpath' in settings else os.getcwd()
file = filedialog.askopenfilename(filetypes=[("BPS Files", ".bps"), ("All Files", "*")], initialdir=initdir)
if file:
settings["patch"] = file
self.patchVar.set(file)
patchSelectButton = Button(patchFileFrame, text='Select Patch', command=PatchSelect)
patchFileLabel.pack(side=LEFT)
patchEntry.pack(side=LEFT, fill=X, expand=True)
patchSelectButton.pack(side=LEFT)
patchFileFrame.pack(fill=X)
def patchRom():
if output_path.cached_path is None:
output_path.cached_path = top.settings["outputpath"]
options = {
"heartbeep": "heartbeep",
"heartcolor": "heartcolor",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes",
"quickswap": "quickswap",
"nobgm": "disablemusic",
"reduce_flashing": "reduce_flashing",
'msu_resume': 'msu_resume',
"shuffle_sfx": "shuffle_sfx",
}
guiargs = Namespace()
for option in options:
arg = options[option]
setattr(guiargs, arg, self.widgets[option].storageVar.get())
guiargs.patch = self.patchVar.get()
guiargs.baserom = top.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
guiargs.sprite = self.sprite
guiargs.outputpath = os.path.dirname(guiargs.patch)
try:
patch(args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
patchButton = Button(self.frames["bottomAdjustFrame"], text='Patch Rom', command=patchRom)
patchButton.pack(side=BOTTOM, padx=(5, 0))
return self,settings return self,settings
+5
View File
@@ -275,4 +275,9 @@ def create_guiargs(parent):
guiargs = update_deprecated_args(guiargs) guiargs = update_deprecated_args(guiargs)
# Key drop shuffle stuff
if guiargs.keydropshuffle:
guiargs.dropshuffle = 1
guiargs.pottery = 'keys' if guiargs.pottery == 'none' else guiargs.pottery
return guiargs return guiargs
+1 -1
View File
@@ -52,7 +52,7 @@ def loadcliargs(gui, args, settings=None):
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label) gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg]) gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg])
# If we're on the Game Options page and it's not about Hints # If we're on the Game Options page and it's not about Hints
if subpage == "gameoptions" and not widget == "hints": if subpage == "gameoptions" and widget not in ["hints", "collection_rate"]:
# Check if we've got settings # Check if we've got settings
# Check if we've got the widget in Adjust settings # Check if we've got the widget in Adjust settings
hasSettings = settings is not None hasSettings = settings is not None
+89 -104
View File
@@ -1,12 +1,13 @@
import RaceRandom as random import RaceRandom as random
import logging import logging
from math import ceil
from collections import defaultdict from collections import defaultdict
from source.item.District import resolve_districts from source.item.District import resolve_districts
from BaseClasses import PotItem, PotFlags
from DoorShuffle import validate_vanilla_reservation from DoorShuffle import validate_vanilla_reservation
from Dungeons import dungeon_table from Dungeons import dungeon_table
from Items import item_table, ItemFactory from Items import item_table, ItemFactory
from PotShuffle import vanilla_pots
class ItemPoolConfig(object): class ItemPoolConfig(object):
@@ -28,7 +29,6 @@ class LocationGroup(object):
# flags # flags
self.keyshuffle = False self.keyshuffle = False
self.keydropshuffle = False
self.shopsanity = False self.shopsanity = False
self.retro = False self.retro = False
@@ -36,9 +36,8 @@ class LocationGroup(object):
self.locations = list(locs) self.locations = list(locs)
return self return self
def flags(self, k, d=False, s=False, r=False): def flags(self, k, s=False, r=False):
self.keyshuffle = k self.keyshuffle = k
self.keydropshuffle = d
self.shopsanity = s self.shopsanity = s
self.retro = r self.retro = r
return self return self
@@ -64,34 +63,40 @@ def create_item_pool_config(world):
config.static_placement = {} config.static_placement = {}
config.location_groups = {} config.location_groups = {}
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
config.static_placement[player] = vanilla_mapping.copy() config.static_placement[player] = defaultdict(list)
if world.keydropshuffle[player]: config.static_placement[player].update(vanilla_mapping)
if world.dropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items(): for item, locs in keydrop_vanilla_mapping.items():
if item in config.static_placement[player]: config.static_placement[player][item].extend(locs)
config.static_placement[player][item].extend(locs) if world.pottery[player] not in ['none', 'cave']:
else: for item, locs in potkeys_vanilla_mapping.items():
config.static_placement[player][item] = list(locs) config.static_placement[player][item].extend(locs)
if world.pottery[player] in ['lottery', 'cave', 'dungeon']:
for super_tile, pot_list in vanilla_pots.items():
for pot_index, pot in enumerate(pot_list):
if pot.item not in [PotItem.Key, PotItem.Hole, PotItem.Switch]:
item = pot_items[pot.item]
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
location = f'{pot.room} {descriptor}'
config.static_placement[player][item].append(location)
if world.shopsanity[player]: if world.shopsanity[player]:
for item, locs in shop_vanilla_mapping.items(): for item, locs in shop_vanilla_mapping.items():
if item in config.static_placement[player]: config.static_placement[player][item].extend(locs)
config.static_placement[player][item].extend(locs)
else:
config.static_placement[player][item] = list(locs)
if world.retro[player]: if world.retro[player]:
for item, locs in retro_vanilla_mapping.items(): for item, locs in retro_vanilla_mapping.items():
if item in config.static_placement[player]: config.static_placement[player][item].extend(locs)
config.static_placement[player][item].extend(locs)
else:
config.static_placement[player][item] = list(locs)
# universal keys # universal keys
universal_key_locations = [] universal_key_locations = []
for item, locs in vanilla_mapping.items(): for item, locs in vanilla_mapping.items():
if 'Small Key' in item: if 'Small Key' in item:
universal_key_locations.extend(locs) universal_key_locations.extend(locs)
if world.keydropshuffle[player]: if world.dropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items(): for item, locs in keydrop_vanilla_mapping.items():
if 'Small Key' in item: if 'Small Key' in item:
universal_key_locations.extend(locs) universal_key_locations.extend(locs)
if world.pottery[player] not in ['none', 'cave']:
for item, locs in potkeys_vanilla_mapping.items():
universal_key_locations.extend(locs)
if world.shopsanity[player]: if world.shopsanity[player]:
single_arrow_placement = list(shop_vanilla_mapping['Red Potion']) single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
single_arrow_placement.append('Red Shield Shop - Right') single_arrow_placement.append('Red Shield Shop - Right')
@@ -117,12 +122,14 @@ def create_item_pool_config(world):
groups = LocationGroup('Major').locs(init_set) groups = LocationGroup('Major').locs(init_set)
if world.bigkeyshuffle[player]: if world.bigkeyshuffle[player]:
groups.locations.extend(mode_grouping['Big Keys']) groups.locations.extend(mode_grouping['Big Keys'])
if world.keydropshuffle[player]: if world.dropshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Big Key Drops']) groups.locations.extend(mode_grouping['Big Key Drops'])
if world.keyshuffle[player]: if world.keyshuffle[player]:
groups.locations.extend(mode_grouping['Small Keys']) groups.locations.extend(mode_grouping['Small Keys'])
if world.keydropshuffle[player]: if world.dropshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Key Drops']) groups.locations.extend(mode_grouping['Key Drops'])
if world.pottery[player] not in ['none', 'cave']:
groups.locations.extend(mode_grouping['Pot Keys'])
if world.compassshuffle[player]: if world.compassshuffle[player]:
groups.locations.extend(mode_grouping['Compasses']) groups.locations.extend(mode_grouping['Compasses'])
if world.mapshuffle[player]: if world.mapshuffle[player]:
@@ -268,7 +275,7 @@ def massage_item_pool(world):
world.itempool.remove(deleted) world.itempool.remove(deleted)
discrepancy -= 1 discrepancy -= 1
if discrepancy > 0: if discrepancy > 0:
logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random') raise Exception(f'Too many required items in pool, {discrepancy} items cannot be placed')
if world.item_pool_config.placeholders is not None: if world.item_pool_config.placeholders is not None:
removed = 0 removed = 0
single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)'] single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)']
@@ -322,49 +329,6 @@ def count_major_items(config, world, player):
return sum(1 for x in world.itempool if x.name in config.item_pool[player] and x.player == player) return sum(1 for x in world.itempool if x.name in config.item_pool[player] and x.player == player)
def calc_dungeon_limits(world, player):
b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player],
world.mapshuffle[player], world.keydropshuffle[player], world.retro[player],
world.restrict_boss_items[player])
if world.doorShuffle[player] in ['vanilla', 'basic']:
limits = {}
for dungeon, info in dungeon_table.items():
val = info.free_items
if bi != 'none' and info.prize:
if bi == 'mapcompass' and (not c or not m):
val -= 1
if bi == 'dungeon' and (not c or not m or not (s or r) or not b):
val -= 1
if b:
val += 1 if info.bk_present else 0
if k:
val += 1 if info.bk_drops else 0
if s or r:
val += info.key_num
if k:
val += info.key_drops
if c:
val += 1 if info.compass_present else 0
if m:
val += 1 if info.map_present else 0
limits[dungeon] = val
else:
limits = 60
if world.bigkeyshuffle[player]:
limits += 11
if world.keydropshuffle[player]:
limits += 1
if world.keyshuffle[player] or world.retro[player]:
limits += 29
if world.keydropshuffle[player]:
limits += 32
if world.compassshuffle[player]:
limits += 11
if world.mapshuffle[player]:
limits += 12
return limits
def determine_major_items(world, player): def determine_major_items(world, player):
major_item_set = set(major_items) major_item_set = set(major_items)
if world.progressive == 'off': if world.progressive == 'off':
@@ -454,6 +418,19 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
return locations return locations
def filter_pot_locations(locations, world):
if world.algorithm == 'district':
config = world.item_pool_config
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name not in restricted or l.player not in restricted[l.name]]
return filtered if len(filtered) > 0 else locations
if world.algorithm == 'vanilla_fill':
filtered = [l for l in locations if l.pot and l.pot.item in [PotItem.Chicken, PotItem.BigMagic]]
return filtered if len(filtered) > 0 else locations
return locations
vanilla_mapping = { vanilla_mapping = {
'Green Pendant': ['Eastern Palace - Prize'], 'Green Pendant': ['Eastern Palace - Prize'],
'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'], 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],
@@ -615,25 +592,31 @@ vanilla_mapping = {
keydrop_vanilla_mapping = { keydrop_vanilla_mapping = {
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key', 'Small Key (Eastern Palace)': ['Eastern Palace - Dark Eyegore Key Drop'],
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'],
'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop'], 'Hyrule Castle - Key Rat Key Drop'],
'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'], 'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'],
'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'], 'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'],
'Small Key (Skull Woods)': ['Skull Woods - Spike Corner Key Drop'],
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop'],
'Small Key (Misery Mire)': ['Misery Mire - Conveyor Crystal Key Drop'],
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
'Small Key (Ganons Tower)': ['Ganons Tower - Mini Helmasaur Key Drop'],
}
potkeys_vanilla_mapping = {
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key'],
'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"], 'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"],
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'], 'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key'],
'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key',
'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key',
'Swamp Palace - Waterway Pot Key'], 'Swamp Palace - Waterway Pot Key'],
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop', 'Small Key (Ice Palace)': ['Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'], 'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key'],
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key',
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'],
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key', 'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key',
'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'], 'Ganons Tower - Conveyor Star Pits Pot Key'],
} }
shop_vanilla_mapping = { shop_vanilla_mapping = {
@@ -682,7 +665,7 @@ mode_grouping = {
'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree', 'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree',
'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45', 'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45',
'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave",
'Chest Game', 'Digging Game', 'Mire Shed - Right', 'Mimic Cave' 'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic Cave'
], ],
'Big Keys': [ 'Big Keys': [
'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest',
@@ -735,7 +718,7 @@ mode_grouping = {
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House', 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House',
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom',
'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right', 'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right',
'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Left' 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Right'
], ],
'GT Trash': [ 'GT Trash': [
'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left',
@@ -751,19 +734,22 @@ mode_grouping = {
], ],
'Key Drops': [ 'Key Drops': [
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop', 'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key', 'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Eyegore Key Drop',
'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop', 'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop',
'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop',
'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Mini Helmasuar Key Drop',
],
'Pot Keys': [
'Eastern Palace - Dark Square Pot Key', 'Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key', 'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key',
'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key", "Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key",
"Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key', 'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key',
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop', 'Misery Mire - Fishbone Pot Key', 'Ganons Tower - Conveyor Cross Pot Key',
'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key',
'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key',
'Ganons Tower - Mini Helmasuar Key Drop',
], ],
'Big Key Drops': ['Hyrule Castle - Big Key Drop'], 'Big Key Drops': ['Hyrule Castle - Big Key Drop'],
'Shops': [ 'Shops': [
@@ -804,25 +790,24 @@ vanilla_swords = {"Link's Uncle", 'Master Sword Pedestal', 'Blacksmith', 'Pyrami
trash_items = { trash_items = {
'Nothing': -1, 'Nothing': -1,
'Bee Trap': 0, 'Bee Trap': 0,
'Rupee (1)': 1, 'Rupee (1)': 1, 'Rupees (5)': 1, 'Small Heart': 1, 'Bee': 1, 'Arrows (5)': 1, 'Chicken': 1, 'Single Bomb': 1,
'Rupees (5)': 1, 'Rupees (20)': 2, 'Small Magic': 2,
'Rupees (20)': 1, 'Bombs (3)': 3, 'Arrows (10)': 3, 'Bombs (10)': 3,
'Big Magic': 4, 'Red Potion': 4, 'Blue Shield': 4, 'Rupees (50)': 4, 'Rupees (100)': 4,
'Small Heart': 2,
'Bee': 2,
'Arrows (5)': 2,
'Chicken': 2,
'Single Bomb': 2,
'Bombs (3)': 3,
'Arrows (10)': 3,
'Bombs (10)': 3,
'Red Potion': 4,
'Blue Shield': 4,
'Rupees (50)': 4,
'Rupees (100)': 4,
'Rupees (300)': 5, 'Rupees (300)': 5,
'Piece of Heart': 17 'Piece of Heart': 17
} }
pot_items = {
PotItem.Nothing: 'Nothing',
PotItem.Bomb: 'Single Bomb',
PotItem.FiveArrows: 'Arrows (5)', # convert to 10
PotItem.OneRupee: 'Rupee (1)',
PotItem.FiveRupees: 'Rupees (5)',
PotItem.Heart: 'Small Heart',
PotItem.BigMagic: 'Big Magic', # fast fill
PotItem.SmallMagic: 'Small Magic',
PotItem.Chicken: 'Chicken' # fast fill
}
valid_pot_items = {y: x for x, y in pot_items.items()}
+41
View File
@@ -0,0 +1,41 @@
import importlib.util
import webbrowser
from tkinter import Tk, Label, Button, Frame
def check_requirements(console=False):
check_packages = {'aenum': 'aenum',
'fast-enum': 'fast_enum',
'python-bps-continued': 'bps',
'colorama': 'colorama',
'aioconsole' : 'aioconsole',
'websockets' : 'websockets',
'pyyaml': 'yaml'}
missing = []
for package, import_name in check_packages.items():
spec = importlib.util.find_spec(import_name)
if spec is None:
missing.append(package)
if len(missing) > 0:
packages = ','.join(missing)
if console:
import logging
logger = logging.getLogger('')
logger.error('You need to install the following python packages:')
logger.error(f'{packages}')
logger.error('See the step about "Installing Platform-specific dependencies":')
logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md')
else:
master = Tk()
master.title('Error')
frame = Frame(master)
frame.pack(expand=True, padx =50, pady=50)
Label(frame, text='You need to install the following python packages:').pack()
Label(frame, text=f'{packages}').pack()
Label(frame, text='See the step about "Installing Platform-specific dependencies":').pack()
url = 'https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md'
link = Label(frame, fg='blue', cursor='hand2', text=url)
link.pack()
link.bind('<Button-1>', lambda e: webbrowser.open_new_tab(url))
Button(master, text='Ok', command=master.destroy).pack()
master.mainloop()
+1 -1
View File
@@ -25,7 +25,7 @@ def main(args=None):
def test(testname: str, command: str): def test(testname: str, command: str):
tests[testname] = [command] tests[testname] = [command]
basecommand = f"python3.8 Mystery.py --suppress_rom" basecommand = f"python3.8 Mystery.py --suppress_rom --suppress_meta"
def gen_seed(): def gen_seed():
taskcommand = basecommand + " " + command taskcommand = basecommand + " " + command
+324
View File
@@ -0,0 +1,324 @@
# Code derived from https://github.com/marcrobledo/RomPatcher.js (MIT License)
import sys
from time import perf_counter
from collections import defaultdict
from binascii import crc32
try:
from fast_enum import FastEnum
except ImportError:
from enum import IntFlag as FastEnum
def bps_get_vlv_len(data):
length = 0
while True:
x = data & 0x7f
data >>= 7
if data == 0:
length += 1
break
length += 1
data -= 1
return length
def bps_read_vlv(stream):
data, shift = 0, 1
while True:
x = stream.read(1)[0]
data += (x & 0x7f) * shift
if x & 0x80:
return data
shift <<= 7
data += shift
class Bps:
def __init__(self):
self.source_size = 0
self.target_size = 0
self.metadata = ''
self.actions = []
self.source_checksum = 0
self.target_checksum = 0
self.patch_checksum = 0
self.binary_ba = bytearray()
self.offset = 0
def write_to_binary(self):
patch_size = 4
patch_size += bps_get_vlv_len(self.source_size)
patch_size += bps_get_vlv_len(self.target_size)
patch_size += bps_get_vlv_len(len(self.metadata))
patch_size += len(self.metadata)
for action in self.actions:
mode, length, data = action
patch_size += bps_get_vlv_len(((length - 1) << 2) + mode)
if mode == BpsMode.BPS_ACTION_TARGET_READ:
patch_size += length
elif mode == BpsMode.BPS_ACTION_SOURCE_COPY or mode == BpsMode.BPS_ACTION_TARGET_COPY:
patch_size += bps_get_vlv_len((abs(data) << 1) + (1 if data < 0 else 0))
patch_size += 12
self.binary_ba = bytearray(patch_size)
self.write_string('BPS1')
self.bps_write_vlv(self.source_size)
self.bps_write_vlv(self.target_size)
self.bps_write_vlv(len(self.metadata))
self.write_string(self.metadata)
for action in self.actions:
mode, length, data = action
self.bps_write_vlv(((length - 1) << 2) + mode)
if mode == BpsMode.BPS_ACTION_TARGET_READ:
self.write_bytes(data)
elif mode == BpsMode.BPS_ACTION_SOURCE_COPY or mode == BpsMode.BPS_ACTION_TARGET_COPY:
self.bps_write_vlv((abs(data) << 1) + (1 if data < 0 else 0))
self.write_u32(self.source_checksum)
self.write_u32(self.target_checksum)
self.write_u32(self.patch_checksum)
def write_string(self, string):
for ch in string:
self.binary_ba[self.offset] = ord(ch)
self.offset += 1
def write_byte(self, byte):
self.binary_ba[self.offset] = byte
self.offset += 1
def write_bytes(self, m_bytes):
for byte in m_bytes:
self.binary_ba[self.offset] = byte
self.offset += 1
def write_u32(self, data):
self.binary_ba[self.offset] = data & 0x000000ff
self.binary_ba[self.offset+1] = (data & 0x0000ff00) >> 8
self.binary_ba[self.offset+2] = (data & 0x00ff0000) >> 16
self.binary_ba[self.offset+3] = (data & 0xff000000) >> 24
self.offset += 4
def bps_write_vlv(self, data):
while True:
x = data & 0x7f
data >>= 7
if data == 0:
self.write_byte(0x80 | x)
break
self.write_byte(x)
data -= 1
class BpsMode(FastEnum):
BPS_ACTION_SOURCE_READ = 0
BPS_ACTION_TARGET_READ = 1
BPS_ACTION_SOURCE_COPY = 2
BPS_ACTION_TARGET_COPY = 3
def create_bps_from_data(original, modified):
patch = Bps()
patch.source_size = len(original)
patch.target_size = len(modified)
patch.actions = create_bps_linear(original, modified)
patch.source_checksum = crc32(original)
patch.target_checksum = crc32(modified)
patch.write_to_binary()
patch.patch_checksum = crc32(patch.binary_ba[:-4])
patch.offset = len(patch.binary_ba) - 4
patch.write_u32(patch.patch_checksum)
return patch
def create_bps_delta(original, modified):
patch_actions = []
source_data = original
target_data = modified
source_size = len(original)
target_size = len(modified)
source_relative_offset = 0
target_relative_offset = 0
output_offset = 0
source_tree = defaultdict(list)
source_tree_2 = defaultdict(list)
target_tree = defaultdict(list)
t1_start = perf_counter()
for offset in range(0, source_size):
symbol = source_data[offset]
if offset < source_size - 1:
symbol |= source_data[offset + 1] << 8
source_tree[symbol].append(offset)
print(f'Elasped Time 1: {perf_counter()-t1_start}')
source_array = list(source_data)
t2_start = perf_counter()
for offset in range(0, source_size):
symbol = source_array[offset]
if offset < source_size - 1:
symbol |= source_array[offset + 1] << 8
source_tree_2[symbol].append(offset)
print(f'Elasped Time 2: {perf_counter()-t2_start}')
trl = {'target_read_length': 0}
def target_read_flush(buffer):
if buffer['target_read_length']:
action = (BpsMode.BPS_ACTION_TARGET_READ, buffer['target_read_length'], [])
patch_actions.append(action)
offset = output_offset - buffer['target_read_length']
while buffer['target_read_length']:
action[2].append(target_data[offset])
offset += 1
buffer['target_read_length'] -= 1
while output_offset < target_size:
max_length, max_offset, mode = 0, 0, BpsMode.BPS_ACTION_TARGET_READ
symbol = target_data[output_offset]
if output_offset < target_size - 1:
symbol |= target_data[output_offset + 1] << 8
# source read
length, offset = 0, output_offset
while offset < source_size and offset < target_size and source_data[offset] == target_data[offset]:
length += 1
offset += 1
if length > max_length:
max_length, mode = length, BpsMode.BPS_ACTION_SOURCE_READ
# source copy
for node in source_tree[symbol]:
length, x, y = 0, node, output_offset
while x < source_size and y < target_size and source_data[x] == target_data[y]:
length += 1
x += 1
y += 1
if length > max_length:
max_length, max_offset, mode = length, node, BpsMode.BPS_ACTION_SOURCE_COPY
# target copy
for node in target_tree[symbol]:
length, x, y = 0, node, output_offset
while y < target_size and target_data[x] == target_data[y]:
length += 1
x += 1
y += 1
if length > max_length:
max_length, max_offset, mode = length, node, BpsMode.BPS_ACTION_TARGET_COPY
target_tree[symbol].append(output_offset)
# target read
if max_length < 4:
max_length = min(1, target_size - output_offset)
mode = BpsMode.BPS_ACTION_TARGET_READ
if mode != BpsMode.BPS_ACTION_TARGET_READ:
target_read_flush(trl)
if mode == BpsMode.BPS_ACTION_SOURCE_READ:
patch_actions.append((mode, max_length, None))
elif mode == BpsMode.BPS_ACTION_TARGET_READ:
trl['target_read_length'] += max_length
else:
if mode == BpsMode.BPS_ACTION_SOURCE_COPY:
relative_offset = max_offset - source_relative_offset
source_relative_offset = max_offset + max_length
else:
relative_offset = max_offset - target_relative_offset
target_relative_offset = max_offset + max_length
patch_actions.append((mode, max_length, relative_offset))
output_offset += max_length
target_read_flush(trl)
return patch_actions
def create_bps_linear(original, modified):
patch_actions = []
source_data = original
target_data = modified
source_size = len(original)
target_size = len(modified)
target_relative_offset = 0
output_offset = 0
trl = {'target_read_length': 0}
def target_read_flush(buffer):
if buffer['target_read_length']:
action = (BpsMode.BPS_ACTION_TARGET_READ, buffer['target_read_length'], [])
patch_actions.append(action)
offset = output_offset - buffer['target_read_length']
while buffer['target_read_length']:
action[2].append(target_data[offset])
offset += 1
buffer['target_read_length'] -= 1
eof = min(source_size, target_size)
while output_offset < target_size:
src_length, n = 0, 0
while output_offset + n < eof:
if source_data[output_offset + n] != target_data[output_offset + n]:
break
src_length += 1
n += 1
rle_length, n = 0, 1
while output_offset + n < target_size:
if target_data[output_offset] != target_data[output_offset + n]:
break
rle_length += 1
n += 1
if rle_length >= 4:
trl['target_read_length'] += 1
output_offset += 1
target_read_flush(trl)
relative_offset = (output_offset - 1) - target_relative_offset
patch_actions.append((BpsMode.BPS_ACTION_TARGET_COPY, rle_length, relative_offset))
output_offset += rle_length
target_relative_offset = output_offset - 1
elif src_length >= 4:
target_read_flush(trl)
patch_actions.append((BpsMode.BPS_ACTION_SOURCE_READ, src_length, None))
output_offset += src_length
else:
trl['target_read_length'] += 1
output_offset += 1
target_read_flush(trl)
return patch_actions
if __name__ == '__main__':
with open(sys.argv[1], 'rb') as source:
sourcedata = source.read()
with open(sys.argv[2], 'rb') as target:
targetdata = target.read()
patch = create_bps_from_data(sourcedata, targetdata)
with open(sys.argv[3], 'wb') as patchfile:
patchfile.write(patch.binary_ba)
+284
View File
@@ -0,0 +1,284 @@
# Demo script showing bias in VT and ER boss shuffle algorithms, with proposed fixes.
import random
boss_location_list = [
'GT_top',
'GT_mid',
'TH',
'SW',
'EP',
'DP',
'PD',
'SP',
'TT',
'IP',
'MM',
'TR',
'GT_bot'
]
entrance_rando_boss_location_list = [
'GT_top',
'TH',
'SW',
'GT_mid',
'EP',
'DP',
'PD',
'SP',
'TT',
'IP',
'MM',
'TR',
'GT_bot'
]
boss_list = [
'Armos',
'Lanmo',
'Moldorm',
'Helma',
'Arrghus',
'Moth',
'Blind',
'Khold',
'Vitty',
'Trinexx'
]
results_dict = {
'GT_top': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'GT_mid': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'TH': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'SW': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'EP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'DP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'PD': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'SP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'TT': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'IP': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'MM': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'TR': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0},
'GT_bot': {'Armos': 0, 'Lanmo': 0, 'Moldorm': 0, 'Helma': 0, 'Arrghus': 0, 'Moth': 0, 'Blind': 0, 'Khold': 0, 'Vitty': 0, 'Trinexx': 0}
}
def can_place_boss(boss:str, dungeon_name: str) -> bool:
if (dungeon_name == 'GT_top'):
if boss in {'Armos', 'Arrghus', 'Blind', 'Trinexx', 'Lanmo'}:
return False
elif (dungeon_name == 'GT_mid'):
if boss == 'Blind':
return False
elif (dungeon_name == 'TH'):
if boss in {'Armos', 'Arrghus', 'Blind', 'Trinexx', 'Lanmo'}:
return False
elif (dungeon_name == 'SW'):
if boss == 'Trinexx':
return False
return True
def resetresults():
for boss_location in boss_location_list:
for boss in boss_list:
results_dict[boss_location][boss] = 0
def printresults(iterations):
for boss_location in boss_location_list:
print("Location: " + boss_location.ljust(6) + ' ', end='')
for boss in boss_list:
percentagestr = "{0:.2%}".format(results_dict[boss_location][boss] / iterations)
print(boss + ": " + percentagestr.rjust(6) + ' ', end='')
restrictive_count = results_dict[boss_location]['Armos'] + results_dict[boss_location]['Arrghus'] + results_dict[boss_location]['Blind'] + results_dict[boss_location]['Trinexx'] + results_dict[boss_location]['Lanmo']
restrictive_pct = "{0:.2%}".format(restrictive_count / iterations)
nonrestrictive_count = results_dict[boss_location]['Moldorm'] + results_dict[boss_location]['Helma'] + results_dict[boss_location]['Moth'] + results_dict[boss_location]['Khold'] + results_dict[boss_location]['Vitty']
nonrestrictive_pct = "{0:.2%}".format(nonrestrictive_count / iterations)
print("Restrictive: " + restrictive_pct.rjust(7) + " Non-restrictive: " + nonrestrictive_pct.rjust(7))
# Demonstration of reshuffle algorithm at:
# https://github.com/sporchia/alttp_vt_randomizer/blob/master/app/Randomizer.php#L399
def vt(reshuffle):
for i in range(iterations):
dupes = random.sample(boss_list, 3)
temp_full_boss_list = boss_list + dupes
shuffled_boss_list = temp_full_boss_list.copy()
random.shuffle(shuffled_boss_list)
for boss_location in boss_location_list:
boss = shuffled_boss_list.pop(0)
while (not can_place_boss(boss, boss_location)):
shuffled_boss_list.append(boss)
# Proposed fix
if reshuffle:
random.shuffle(shuffled_boss_list)
boss = shuffled_boss_list.pop(0)
results_dict[boss_location][boss] = results_dict[boss_location][boss] + 1
# Demonstration of reshuffle algorithm at:
# https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/Dev/Bosses.py#L203
def er(reshuffle):
for i in range(iterations):
dupes = random.sample(boss_list, 3)
temp_full_boss_list = boss_list + dupes
shuffled_boss_list = temp_full_boss_list.copy()
random.shuffle(shuffled_boss_list)
for boss_location in entrance_rando_boss_location_list:
boss = next((b for b in shuffled_boss_list if can_place_boss(b, boss_location)), None)
shuffled_boss_list.remove(boss)
results_dict[boss_location][boss] = results_dict[boss_location][boss] + 1
# Proposed fix
if reshuffle:
random.shuffle(shuffled_boss_list)
def er_unique():
for i in range(iterations):
temp_full_boss_list = boss_list
moldorm2 = random.choice([b for b in boss_list
if b not in ["Armos", "Arrghus", "Blind", "Trinexx", "Lanmo"]])
lanmo2 = random.choice([b for b in boss_list
if b not in [moldorm2, "Blind"]])
armos2 = random.choice([b for b in boss_list if b not in [moldorm2, lanmo2]])
gt_bosses = [moldorm2, lanmo2, armos2]
shuffled_boss_list = temp_full_boss_list.copy()
for boss_location in entrance_rando_boss_location_list:
if '_' in boss_location:
boss = random.choice([b for b in gt_bosses if can_place_boss(b, boss_location)])
gt_bosses.remove(boss)
else:
boss = random.choice([b for b in shuffled_boss_list if can_place_boss(b, boss_location)])
shuffled_boss_list.remove(boss)
results_dict[boss_location][boss] += 1
def er_unique_chaos():
for i in range(iterations):
temp_full_boss_list = boss_list
shuffled_boss_list = temp_full_boss_list.copy()
for boss_location in entrance_rando_boss_location_list:
if '_' in boss_location:
boss = random.choice(b for b in boss_list if can_place_boss(b, boss_location))
else:
boss = random.choice(b for b in shuffled_boss_list if can_place_boss(b, boss_location))
shuffled_boss_list.remove(boss)
results_dict[boss_location][boss] += 1
if __name__ == '__main__':
iterations = 100000
# reshuffle = False
#
# vt(reshuffle)
#
# print("Original results:")
#
# printresults(iterations)
# resetresults()
#
# reshuffle = True
#
# vt(reshuffle)
#
# print("")
# print("Reshuffled results:")
#
# printresults(iterations)
# resetresults()
#
# #ER
# reshuffle = False
# er(reshuffle)
#
# print("")
# print("ER Original results:")
#
# printresults(iterations)
# resetresults()
#
# reshuffle = True
# er(reshuffle)
#
# print("")
# print("ER Reshuffled results:")
#
# printresults(iterations)
# resetresults()
er_unique()
print("")
print("ER Unique results:")
printresults(iterations)
resetresults()
er_unique_chaos()
print("")
print("ER Chaos results:")
printresults(iterations)
# Results:
#Original results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.00% Helma: 19.89% Arrghus: 0.00% Moth: 20.01% Blind: 0.00% Khold: 20.14% Vitty: 19.96% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 11.05% Lanmo: 11.16% Moldorm: 11.00% Helma: 11.35% Arrghus: 11.07% Moth: 11.00% Blind: 0.00% Khold: 11.01% Vitty: 11.20% Trinexx: 11.15% Restrictive: 44.43% Non-restrictive: 55.57%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 20.06% Helma: 19.95% Arrghus: 0.00% Moth: 20.07% Blind: 0.00% Khold: 19.88% Vitty: 20.05% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 11.30% Lanmo: 11.04% Moldorm: 11.30% Helma: 10.95% Arrghus: 11.09% Moth: 11.04% Blind: 11.15% Khold: 11.15% Vitty: 10.98% Trinexx: 0.00% Restrictive: 44.59% Non-restrictive: 55.42%
#Location: EP Armos: 9.89% Lanmo: 9.90% Moldorm: 10.02% Helma: 9.97% Arrghus: 10.17% Moth: 9.98% Blind: 9.95% Khold: 10.16% Vitty: 9.90% Trinexx: 10.07% Restrictive: 49.97% Non-restrictive: 50.03%
#Location: DP Armos: 9.98% Lanmo: 10.09% Moldorm: 9.97% Helma: 9.95% Arrghus: 10.17% Moth: 10.01% Blind: 9.87% Khold: 10.02% Vitty: 9.88% Trinexx: 10.04% Restrictive: 50.16% Non-restrictive: 49.84%
#Location: PD Armos: 10.12% Lanmo: 9.96% Moldorm: 9.85% Helma: 9.90% Arrghus: 10.31% Moth: 9.91% Blind: 9.96% Khold: 9.87% Vitty: 9.94% Trinexx: 10.17% Restrictive: 50.52% Non-restrictive: 49.48%
#Location: SP Armos: 10.41% Lanmo: 10.35% Moldorm: 9.61% Helma: 9.66% Arrghus: 10.29% Moth: 9.60% Blind: 10.47% Khold: 9.67% Vitty: 9.53% Trinexx: 10.42% Restrictive: 51.93% Non-restrictive: 48.07%
#Location: TT Armos: 11.04% Lanmo: 10.98% Moldorm: 8.86% Helma: 8.95% Arrghus: 10.98% Moth: 9.12% Blind: 11.18% Khold: 8.98% Vitty: 9.03% Trinexx: 10.89% Restrictive: 55.06% Non-restrictive: 44.94%
#Location: IP Armos: 12.10% Lanmo: 11.89% Moldorm: 7.81% Helma: 7.83% Arrghus: 11.94% Moth: 7.89% Blind: 12.84% Khold: 7.87% Vitty: 7.99% Trinexx: 11.84% Restrictive: 60.62% Non-restrictive: 39.38%
#Location: MM Armos: 13.67% Lanmo: 13.66% Moldorm: 6.00% Helma: 6.17% Arrghus: 13.48% Moth: 6.18% Blind: 15.06% Khold: 6.25% Vitty: 6.14% Trinexx: 13.41% Restrictive: 69.28% Non-restrictive: 30.72%
#Location: TR Armos: 15.35% Lanmo: 15.49% Moldorm: 3.87% Helma: 3.90% Arrghus: 15.31% Moth: 3.78% Blind: 19.06% Khold: 3.89% Vitty: 3.82% Trinexx: 15.53% Restrictive: 80.74% Non-restrictive: 19.26%
#Location: GT_bot Armos: 15.24% Lanmo: 15.24% Moldorm: 1.54% Helma: 1.51% Arrghus: 15.22% Moth: 1.52% Blind: 20.44% Khold: 1.51% Vitty: 1.53% Trinexx: 26.25% Restrictive: 92.38% Non-restrictive: 7.62%
#
#Reshuffled results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.02% Helma: 19.97% Arrghus: 0.00% Moth: 20.02% Blind: 0.00% Khold: 20.07% Vitty: 19.92% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 12.23% Lanmo: 12.05% Moldorm: 10.23% Helma: 10.24% Arrghus: 12.11% Moth: 10.31% Blind: 0.00% Khold: 10.36% Vitty: 10.33% Trinexx: 12.13% Restrictive: 48.52% Non-restrictive: 51.48%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 19.86% Helma: 20.09% Arrghus: 0.00% Moth: 20.23% Blind: 0.00% Khold: 19.83% Vitty: 19.99% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 13.23% Lanmo: 13.34% Moldorm: 9.05% Helma: 9.02% Arrghus: 13.37% Moth: 9.10% Blind: 14.97% Khold: 8.98% Vitty: 8.93% Trinexx: 0.00% Restrictive: 54.92% Non-restrictive: 45.08%
#Location: EP Armos: 11.54% Lanmo: 11.50% Moldorm: 7.96% Helma: 7.82% Arrghus: 11.76% Moth: 7.81% Blind: 12.84% Khold: 7.79% Vitty: 8.03% Trinexx: 12.95% Restrictive: 60.59% Non-restrictive: 39.41%
#Location: DP Armos: 11.55% Lanmo: 11.49% Moldorm: 7.93% Helma: 7.85% Arrghus: 11.72% Moth: 7.69% Blind: 12.80% Khold: 7.99% Vitty: 7.74% Trinexx: 13.25% Restrictive: 60.80% Non-restrictive: 39.20%
#Location: PD Armos: 11.79% Lanmo: 11.47% Moldorm: 7.81% Helma: 7.78% Arrghus: 11.61% Moth: 7.91% Blind: 12.81% Khold: 7.78% Vitty: 8.00% Trinexx: 13.02% Restrictive: 60.71% Non-restrictive: 39.29%
#Location: SP Armos: 11.59% Lanmo: 11.56% Moldorm: 7.90% Helma: 7.75% Arrghus: 11.52% Moth: 8.02% Blind: 12.67% Khold: 7.92% Vitty: 7.85% Trinexx: 13.23% Restrictive: 60.57% Non-restrictive: 39.43%
#Location: TT Armos: 11.64% Lanmo: 11.64% Moldorm: 7.77% Helma: 7.73% Arrghus: 11.70% Moth: 7.67% Blind: 12.89% Khold: 8.05% Vitty: 7.85% Trinexx: 13.07% Restrictive: 60.93% Non-restrictive: 39.07%
#Location: IP Armos: 11.54% Lanmo: 11.77% Moldorm: 7.95% Helma: 7.97% Arrghus: 11.49% Moth: 7.80% Blind: 12.64% Khold: 7.87% Vitty: 7.89% Trinexx: 13.10% Restrictive: 60.54% Non-restrictive: 39.47%
#Location: MM Armos: 11.69% Lanmo: 11.65% Moldorm: 7.91% Helma: 7.88% Arrghus: 11.59% Moth: 7.78% Blind: 12.80% Khold: 7.73% Vitty: 7.92% Trinexx: 13.04% Restrictive: 60.77% Non-restrictive: 39.23%
#Location: TR Armos: 11.58% Lanmo: 11.82% Moldorm: 7.75% Helma: 7.83% Arrghus: 11.62% Moth: 7.79% Blind: 12.76% Khold: 7.90% Vitty: 7.76% Trinexx: 13.19% Restrictive: 60.97% Non-restrictive: 39.03%
#Location: GT_bot Armos: 11.54% Lanmo: 11.60% Moldorm: 7.91% Helma: 7.87% Arrghus: 11.62% Moth: 7.90% Blind: 12.78% Khold: 7.82% Vitty: 7.86% Trinexx: 13.10% Restrictive: 60.65% Non-restrictive: 39.35%
#
#ER Original results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 19.94% Helma: 19.87% Arrghus: 0.00% Moth: 19.98% Blind: 0.00% Khold: 20.31% Vitty: 19.90% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 14.15% Lanmo: 14.33% Moldorm: 4.51% Helma: 4.53% Arrghus: 14.19% Moth: 4.56% Blind: 0.00% Khold: 4.61% Vitty: 4.49% Trinexx: 34.62% Restrictive: 77.30% Non-restrictive: 22.70%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 19.93% Helma: 19.98% Arrghus: 0.00% Moth: 20.10% Blind: 0.00% Khold: 19.75% Vitty: 20.24% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 21.52% Lanmo: 21.56% Moldorm: 2.76% Helma: 2.80% Arrghus: 21.50% Moth: 2.82% Blind: 21.48% Khold: 2.81% Vitty: 2.75% Trinexx: 0.00% Restrictive: 86.05% Non-restrictive: 13.95%
#Location: EP Armos: 11.39% Lanmo: 11.33% Moldorm: 5.89% Helma: 5.69% Arrghus: 11.32% Moth: 5.83% Blind: 24.71% Khold: 5.82% Vitty: 5.76% Trinexx: 12.27% Restrictive: 71.01% Non-restrictive: 28.99%
#Location: DP Armos: 11.62% Lanmo: 11.66% Moldorm: 8.05% Helma: 8.32% Arrghus: 11.75% Moth: 8.13% Blind: 12.46% Khold: 8.10% Vitty: 8.18% Trinexx: 11.73% Restrictive: 59.22% Non-restrictive: 40.78%
#Location: PD Armos: 11.01% Lanmo: 10.77% Moldorm: 9.18% Helma: 9.20% Arrghus: 10.89% Moth: 9.03% Blind: 10.83% Khold: 9.13% Vitty: 9.17% Trinexx: 10.79% Restrictive: 54.29% Non-restrictive: 45.71%
#Location: SP Armos: 10.26% Lanmo: 10.17% Moldorm: 9.73% Helma: 9.61% Arrghus: 10.33% Moth: 9.69% Blind: 10.31% Khold: 9.84% Vitty: 9.61% Trinexx: 10.45% Restrictive: 51.52% Non-restrictive: 48.48%
#Location: TT Armos: 10.12% Lanmo: 10.01% Moldorm: 10.07% Helma: 9.97% Arrghus: 10.16% Moth: 9.92% Blind: 9.95% Khold: 9.73% Vitty: 9.96% Trinexx: 10.11% Restrictive: 50.35% Non-restrictive: 49.65%
#Location: IP Armos: 10.03% Lanmo: 10.01% Moldorm: 10.00% Helma: 10.10% Arrghus: 9.89% Moth: 9.93% Blind: 10.13% Khold: 9.99% Vitty: 9.80% Trinexx: 10.13% Restrictive: 50.18% Non-restrictive: 49.82%
#Location: MM Armos: 9.89% Lanmo: 9.99% Moldorm: 10.09% Helma: 10.10% Arrghus: 10.02% Moth: 9.93% Blind: 10.03% Khold: 10.00% Vitty: 10.06% Trinexx: 9.89% Restrictive: 49.81% Non-restrictive: 50.19%
#Location: TR Armos: 10.03% Lanmo: 10.10% Moldorm: 10.01% Helma: 9.92% Arrghus: 9.94% Moth: 10.01% Blind: 10.04% Khold: 9.95% Vitty: 9.92% Trinexx: 10.08% Restrictive: 50.19% Non-restrictive: 49.81%
#Location: GT_bot Armos: 9.90% Lanmo: 10.04% Moldorm: 9.88% Helma: 9.85% Arrghus: 10.00% Moth: 10.04% Blind: 10.09% Khold: 10.09% Vitty: 10.11% Trinexx: 10.01% Restrictive: 50.03% Non-restrictive: 49.97%
#
#ER Reshuffled results:
#Location: GT_top Armos: 0.00% Lanmo: 0.00% Moldorm: 20.11% Helma: 19.81% Arrghus: 0.00% Moth: 20.22% Blind: 0.00% Khold: 20.08% Vitty: 19.78% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: GT_mid Armos: 13.17% Lanmo: 13.37% Moldorm: 9.05% Helma: 9.14% Arrghus: 13.23% Moth: 9.29% Blind: 0.00% Khold: 8.96% Vitty: 9.11% Trinexx: 14.69% Restrictive: 54.45% Non-restrictive: 45.55%
#Location: TH Armos: 0.00% Lanmo: 0.00% Moldorm: 20.03% Helma: 19.72% Arrghus: 0.00% Moth: 20.17% Blind: 0.00% Khold: 19.86% Vitty: 20.23% Trinexx: 0.00% Restrictive: 0.00% Non-restrictive: 100.00%
#Location: SW Armos: 13.38% Lanmo: 13.41% Moldorm: 9.27% Helma: 9.33% Arrghus: 13.41% Moth: 9.27% Blind: 13.48% Khold: 9.28% Vitty: 9.17% Trinexx: 0.00% Restrictive: 53.69% Non-restrictive: 46.31%
#Location: EP Armos: 11.50% Lanmo: 11.56% Moldorm: 7.90% Helma: 8.06% Arrghus: 11.58% Moth: 8.11% Blind: 12.72% Khold: 7.93% Vitty: 7.93% Trinexx: 12.70% Restrictive: 60.06% Non-restrictive: 39.94%
#Location: DP Armos: 11.66% Lanmo: 11.51% Moldorm: 8.05% Helma: 7.96% Arrghus: 11.43% Moth: 7.83% Blind: 12.94% Khold: 7.89% Vitty: 7.88% Trinexx: 12.87% Restrictive: 60.40% Non-restrictive: 39.60%
#Location: PD Armos: 11.36% Lanmo: 11.53% Moldorm: 7.89% Helma: 7.96% Arrghus: 11.46% Moth: 7.72% Blind: 13.12% Khold: 8.07% Vitty: 8.02% Trinexx: 12.87% Restrictive: 60.34% Non-restrictive: 39.66%
#Location: SP Armos: 11.27% Lanmo: 11.38% Moldorm: 7.89% Helma: 7.98% Arrghus: 11.56% Moth: 7.89% Blind: 13.10% Khold: 8.17% Vitty: 8.08% Trinexx: 12.68% Restrictive: 59.99% Non-restrictive: 40.01%
#Location: TT Armos: 11.32% Lanmo: 11.49% Moldorm: 7.91% Helma: 8.16% Arrghus: 11.44% Moth: 7.97% Blind: 12.93% Khold: 8.05% Vitty: 7.97% Trinexx: 12.77% Restrictive: 59.94% Non-restrictive: 40.06%
#Location: IP Armos: 11.69% Lanmo: 11.74% Moldorm: 7.91% Helma: 8.00% Arrghus: 11.34% Moth: 7.87% Blind: 12.80% Khold: 7.82% Vitty: 7.95% Trinexx: 12.88% Restrictive: 60.44% Non-restrictive: 39.56%
#Location: MM Armos: 11.57% Lanmo: 11.31% Moldorm: 8.13% Helma: 7.90% Arrghus: 11.32% Moth: 7.91% Blind: 13.15% Khold: 7.97% Vitty: 7.82% Trinexx: 12.91% Restrictive: 60.26% Non-restrictive: 39.74%
#Location: TR Armos: 11.44% Lanmo: 11.30% Moldorm: 8.04% Helma: 8.13% Arrghus: 11.47% Moth: 7.90% Blind: 12.82% Khold: 8.09% Vitty: 7.96% Trinexx: 12.84% Restrictive: 59.87% Non-restrictive: 40.13%
#Location: GT_bot Armos: 11.36% Lanmo: 11.53% Moldorm: 7.87% Helma: 7.99% Arrghus: 11.49% Moth: 7.96% Blind: 13.01% Khold: 7.98% Vitty: 8.00% Trinexx: 12.80% Restrictive: 60.19% Non-restrictive: 39.81
View File
+26
View File
@@ -0,0 +1,26 @@
meta:
# seed: 872603231
seed: 222336029
settings:
1:
door_shuffle: crossed
# door_shuffle: vanilla
mode: standard
shufflebosses: unique
doors:
1:
doors:
Hyrule Dungeon Cellblock Up Stairs: Thieves Basement Block Up Stairs
bosses:
1:
Thieves Town: Moldorm
#start_inventory:
# 1:
# - Titans Mitts
# - Ocarina
# - Moon Pearl
# - Tempered Sword
# - Boss Heart Container
# - Boss Heart Container
# - Boss Heart Container
# - Boss Heart Container