56 Commits

Author SHA1 Message Date
4468721621 Remove another reference to unused base2current.json 2026-01-25 14:38:13 -06:00
becff60e05 Remove commented out data in SpriteSheets.py 2026-01-25 14:31:09 -06:00
1edd600272 Merge branch 'main' into beta 2026-01-25 14:22:29 -06:00
a019fa6478 romname starts with GK 2026-01-25 13:16:35 -06:00
a51be12a4a More romname fixes 2026-01-25 10:34:37 -06:00
9e5db19c73 Change order of parts in rom name for compatibility with webmulti 2026-01-25 10:11:10 -06:00
640a5bc3a7 Log exception trace on failed generation attempts 2026-01-24 22:16:47 -06:00
c946981c38 isort 2026-01-24 22:09:56 -06:00
9733da9f44 Always include team number in rom name 2026-01-24 21:59:11 -06:00
a3f67a39ab Version/romnaming refactor 2026-01-24 20:51:48 -06:00
1d4c5f1884 .gitattributes 2026-01-24 13:56:09 -06:00
a8dc25e59d Remove unnecessary references to data/base2current.json 2026-01-24 13:47:53 -06:00
0f7253c94e Reduce tier of triforce pieces when goal is high 2026-01-23 21:41:45 -06:00
ddda8aeddf Guarantee a bee for sale 2026-01-23 18:56:20 -06:00
ae1744f0dd Update baserom 2026-01-21 19:47:43 -06:00
f2d07c98b7 Update baserom - kak music bug 2026-01-21 18:26:04 -06:00
67307a872c Bosshunt mode 2026-01-21 08:20:59 -06:00
3255c75828 Update baserom 2026-01-19 13:37:08 -06:00
9db2a3b64b Update baserom 2026-01-18 16:57:23 -06:00
d424505677 Add attempt number to failure messages 2026-01-18 00:39:53 -06:00
2a7141d24e Update baserom 2026-01-17 13:57:09 -06:00
80a64b884e Update baserom 2026-01-17 13:31:10 -06:00
5e0deadf55 showmap option 2026-01-17 12:54:17 -06:00
dd98e55d36 Move dungeon balancing message to debug level 2026-01-16 17:55:45 -06:00
6c8a8e18e0 Add --tries arg 2026-01-16 16:43:12 -06:00
e058fd1961 Update baserom 2026-01-16 14:38:16 -06:00
bfd791fc87 Update baserom 2026-01-16 12:28:40 -06:00
b10c700836 Tweak loot/map values slightly 2026-01-15 19:03:51 -06:00
f5d5860af0 Update baserom 2026-01-15 13:52:41 -06:00
da0cef6dfe Update baserom 2026-01-15 13:47:20 -06:00
36eb171d24 Merge branch 'GwaaKiwi' into DungeonMap 2026-01-15 04:08:13 -06:00
2ce245c430 Update baserom 2026-01-14 18:23:10 -06:00
e8bc0d81c4 Update baserom 2026-01-14 13:10:48 -06:00
5f66483d98 Update baserom 2026-01-14 11:33:53 -06:00
30e85c1256 Update baserom 2026-01-13 19:45:10 -06:00
402c9a5f76 Update baserom 2026-01-13 18:58:30 -06:00
34b5dfb4e8 Update baserom 2026-01-13 11:01:07 -06:00
54e73c0b99 Update baserom 2026-01-13 09:34:46 -06:00
affa210802 Update baserom 2026-01-13 00:34:45 -06:00
ea62a5bcb7 Update baserom, write better doors map flags 2026-01-13 00:27:27 -06:00
bd5fd9ec56 Update baserom, fix freezor chest? 2026-01-10 14:53:58 -06:00
8df39da2bd Add showloot setting 2026-01-08 08:18:27 -06:00
9b98f59292 Update baserom 2026-01-07 20:57:12 -06:00
870d981eab Update baserom 2026-01-06 22:40:41 -06:00
a895fe158b Update baserom (should fix bad floor ids messing things up in doors) 2026-01-06 10:43:30 -06:00
409c6d9a4e Update baserom 2026-01-04 23:04:09 -06:00
42b4374bd3 Initial pass at door shuffle dungeon maps 2026-01-04 14:24:29 -06:00
74739347e7 Update baserom 2026-01-04 12:58:18 -06:00
c94d556c4b Baserom update 2026-01-04 12:26:50 -06:00
2195adca64 Add HC and AT compass if pots or enemies are shuffled 2026-01-04 09:44:29 -06:00
e5ad81861a Update baserom 2026-01-04 09:19:05 -06:00
5418dbfb88 Update baserom, set item loot levels for crystals, pendants, and bomb bag 2026-01-04 07:17:24 -06:00
507f4170c8 Update Baserom and write loot bits 2026-01-03 23:19:51 -06:00
b038ee6cbe Update baserom 2026-01-03 17:33:34 -06:00
2d91647794 Update baserom 2026-01-03 17:28:56 -06:00
7d95405252 Update baserom 2026-01-03 17:17:11 -06:00
29 changed files with 669 additions and 560 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto

2
.gitignore vendored
View File

@@ -32,8 +32,6 @@ weights/
/output/
/enemizer/
base2current.json
resources/user/*
!resources/user/.gitkeep

View File

@@ -21,6 +21,7 @@ from Tables import (
spiral_offset_table,
)
from Utils import int16_as_bytes
from Versions import DRVersion, GKVersion, ORVersion
class World(object):
@@ -79,6 +80,8 @@ class World(object):
self.dark_rooms = {}
self.damage_challenge = {}
self.shuffle_damage_table = {}
self.bosses_ganon = {}
self.bosshunt_include_agas = {}
self.ganon_item = {}
self.ganon_item_orig = {}
self.custom = custom
@@ -154,6 +157,8 @@ class World(object):
set_player_attr('keyshuffle', 'none')
set_player_attr('bigkeyshuffle', 'none')
set_player_attr('prizeshuffle', 'none')
set_player_attr('showloot', 'never')
set_player_attr('showmap', 'map')
set_player_attr('restrict_boss_items', 'none')
set_player_attr('bombbag', False)
set_player_attr('flute_mode', 'normal')
@@ -171,6 +176,8 @@ class World(object):
set_player_attr('escape_assist', [])
set_player_attr('crystals_needed_for_ganon', 7)
set_player_attr('crystals_needed_for_gt', 7)
set_player_attr('bosses_ganon', 8)
set_player_attr('bosshunt_include_agas', False)
set_player_attr('ganon_item', 'silver')
set_player_attr('crystals_ganon_orig', {})
set_player_attr('crystals_gt_orig', {})
@@ -359,7 +366,7 @@ class World(object):
else:
if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district']:
return False
elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']:
elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt', 'bosshunt']:
return True
else:
return False
@@ -3077,12 +3084,9 @@ class Spoiler(object):
self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)])
def parse_meta(self):
from Main import __version__ as ERVersion
from OverworldShuffle import __version__ as ORVersion
self.startinventory = list(map(str, self.world.precollected_items))
self.metadata = {'version': ERVersion,
'versions': {'Door':ERVersion, 'Overworld':ORVersion},
self.metadata = {'version': GKVersion,
'versions': {'Door': DRVersion, 'Overworld': ORVersion},
'logic': self.world.logic,
'mode': self.world.mode,
'bombbag': self.world.bombbag,
@@ -3121,6 +3125,8 @@ class Spoiler(object):
'beemizer': self.world.beemizer,
'gt_crystals': self.world.crystals_needed_for_gt,
'ganon_crystals': self.world.crystals_needed_for_ganon,
'ganon_bosses': self.world.bosses_ganon,
'bosshunt_include_agas': self.world.bosshunt_include_agas,
'ganon_item': self.world.ganon_item,
'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility,
@@ -3131,6 +3137,8 @@ class Spoiler(object):
'keyshuffle': self.world.keyshuffle,
'bigkeyshuffle': self.world.bigkeyshuffle,
'prizeshuffle': self.world.prizeshuffle,
'showloot': self.world.showloot,
'showmap': self.world.showmap,
'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health,
@@ -3295,7 +3303,7 @@ class Spoiler(object):
self.parse_meta()
with open(filename, 'w') as outfile:
line_width = 35
outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed))
outfile.write('ALttP GwaaKiwi 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):
@@ -3310,7 +3318,7 @@ class Spoiler(object):
self.parse_meta()
with open(filename, 'w') as outfile:
line_width = 35
outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed))
outfile.write('ALttP GwaaKiwi 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)
if self.metadata['user_notes']:
@@ -3341,6 +3349,10 @@ class Spoiler(object):
if custom['ganongoal'] and 'requirements' in custom['ganongoal']:
outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n')
outfile.write(' %s\n' % custom['ganongoal']['goaltext'])
elif self.metadata['goal'][player] == 'bosshunt':
outfile.write('Ganon Requirement:'.ljust(line_width) + '%s bosses%s\n' %
(str(self.world.bosses_ganon[player]),
' (including both Agahnims)' if self.world.bosshunt_include_agas[player] else ''))
else:
outfile.write('Ganon Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_ganon_orig[player]))
if custom['pedgoal'] and 'requirements' in custom['pedgoal']:
@@ -3395,6 +3407,8 @@ class Spoiler(object):
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player])
outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][player])
outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player])
outfile.write('Show Value of Checks:'.ljust(line_width) + '%s\n' % self.metadata['showloot'][player])
outfile.write('Show Map:'.ljust(line_width) + '%s\n' % self.metadata['showmap'][player])
outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player])
outfile.write('\n')
outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player])
@@ -3748,8 +3762,9 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2}
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
# byte 2: GGGD DFFH (goal, diff, item_func, hints)
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5,
'ganonhunt': 6, 'completionist': 7, 'sanctuary': 1}
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3,
'crystals': 4, 'trinity': 5, 'ganonhunt': 6, 'completionist': 7,
'sanctuary': 1, 'bosshunt': 6}
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
func_mode = {"normal": 0, "hard": 1, "expert": 2}

10
CLI.py
View File

@@ -106,7 +106,7 @@ def parse_cli(argv, no_defaults=False):
ret = parser.parse_args(argv)
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = 'wild' * 4
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = ['wild'] * 4
if ret.keydropshuffle:
ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle
@@ -133,8 +133,8 @@ def parse_cli(argv, no_defaults=False):
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle',
'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'bosshunt_include_agas', 'ganon_item', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', 'startinventory',
'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern',
@@ -177,6 +177,8 @@ def parse_settings():
"goal": "ganon",
"crystals_gt": "7",
"crystals_ganon": "7",
"bosses_ganon": "8",
"bosshunt_include_agas": False,
"ganon_item": "silver",
"swords": "random",
"flute_mode": "normal",
@@ -234,6 +236,8 @@ def parse_settings():
"keyshuffle": "none",
"bigkeyshuffle": "none",
"prizeshuffle": "none",
"showloot": "never",
"showmap": "map",
"keysanity": False,
"door_shuffle": "vanilla",
"intensity": 3,

View File

@@ -2340,7 +2340,7 @@ def parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole):
increment_depth = True
current_depth = last_depth + 1 if increment_depth else last_depth
finished = all([(x.polarity()+sum_polarity(solution_list[x])).is_neutral() for x in builders])
logging.getLogger('').info(f'-Balanced solution found in {time.process_time()-start}')
logging.getLogger('').debug(f'-Balanced solution found in {time.process_time()-start}')
for builder, sectors in solution_list.items():
for sector in sectors:
assign_sector(sector, builder, polarized_sectors, global_pole)

View File

@@ -11,7 +11,7 @@ import RaceRandom as random
import source.classes.diags as diagnostics
from CLI import get_args_priority, parse_cli
from Fill import FillError
from Main import EnemizerError, __version__, main
from Main import EnemizerError, main
from Rom import get_sprite_from_name
from source.classes.BabelFish import BabelFish
from Utils import close_console, is_bundled
@@ -80,7 +80,7 @@ def start():
break
except (FillError, EnemizerError, Exception, RuntimeError) as err:
failures.append((err, seed))
logger.warning('%s: %s', fish.translate("cli","cli","generation.failed"), err)
logger.exception('Attempt %d - %s: %s', trynum, fish.translate("cli","cli","generation.failed"), err)
logger.info('')
seed = random.randint(0, 999999999)

View File

@@ -13,7 +13,19 @@ def create_dungeons(world, player):
dungeon.world = world
return dungeon
ES = make_dungeon('Hyrule Castle', 1, None, hyrule_castle_regions, None, [ItemFactory('Small Key (Escape)', player)], [ItemFactory('Map (Escape)', player)])
hc_dungeon_items = ['Map (Escape)']
at_dungeon_items = []
if world.showloot[player] == 'compass':
if world.dropshuffle[player] == 'underworld' or world.pottery[player] in ['dungeon', 'reduced', 'clustered', 'nonempty', 'lottery']:
hc_dungeon_items.append('Compass (Escape)')
at_dungeon_items.append('Compass (Agahnims Tower)')
elif world.compassshuffle[player] == 'wild':
hc_dungeon_items.append('Compass (Escape)')
if world.keyshuffle[player] == 'wild':
at_dungeon_items.append('Compass (Agahnims Tower)')
ES = make_dungeon('Hyrule Castle', 1, None, hyrule_castle_regions, None, [ItemFactory('Small Key (Escape)', player)], ItemFactory(hc_dungeon_items, player))
EP = make_dungeon('Eastern Palace', 2, 'Armos Knights', eastern_regions, ItemFactory('Big Key (Eastern Palace)', player), [], ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
DP = make_dungeon('Desert Palace', 3, 'Lanmolas', desert_regions, ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
ToH = make_dungeon('Tower of Hera', 10, 'Moldorm', hera_regions, ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
@@ -24,7 +36,7 @@ def create_dungeons(world, player):
IP = make_dungeon('Ice Palace', 9, 'Kholdstare', ice_regions, ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
MM = make_dungeon('Misery Mire', 7, 'Vitreous', mire_regions, ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 12, 'Trinexx', tr_regions, ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
AT = make_dungeon('Agahnims Tower', 4, 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
AT = make_dungeon('Agahnims Tower', 4, 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), ItemFactory(at_dungeon_items, player))
GT = make_dungeon('Ganons Tower', 13, 'Agahnim2', gt_regions, ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
GT.bosses['bottom'] = BossFactory('Armos Knights', player)

View File

@@ -1,361 +1,361 @@
Hint description:
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
4 hints for inconvenient entrances.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
4 junk hints.
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
5 hints for inconvenient item locations.
8 hints for valuable items.
7 junk hints.
In the simple, restricted shuffles, these are the ratios:
2 hints for inconvenient entrances.
1 hint for an inconvenient dungeon entrance.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
5 junk hints.
These hints will use the following format:
Entrance hints go "[Entrance on overworld] leads to [interior]".
Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass).
The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots.
Hint Locations:
Eastern Palace room before Big Chest
Desert Palace bonk torch room
Tower of Hera entrance room
Tower of Hera Big Chest room
Castle Tower after dark rooms
Palace of Darkness before Bow section
Swamp Palace entryway
Thieves' Town upstairs
Ice Palace entrance
Ice Palace after first drop
Ice Palace tall ice floor room
Misery Mire cutscene room
Turtle Rock entrance
Spectacle Rock cave
Spiky Hint cave
PoD Bdlg NPC
Near PoD Storyteller (bug near bomb wall)
Dark Sanctuary Storyteller (long room with tables)
Near Mire Storyteller (feather duster in winding cave)
SE DW Storyteller (owl in winding cave)
Inconvenient entrance list:
Skull Woods Final
Ice Palace
Misery Mire
Turtle Rock
Ganon's Tower
Mimic Ledge
SW DM Foothills Cave (mirror from upper Bumper ledge)
Hammer Pegs (near purple chest)
Super Bomb cracked wall
Inconvenient location list:
Swamp left (two chests)
Mire left (two chests)
Hera basement
Eastern Palace Big Key chest (protected by anti-fairies)
Thieves' Town Big Chest
Ice Palace Big Chest
Ganon's Tower Big Chest
Purple Chest
Spike Cave
Magic Bat
Sahasrahla (Green Pendant)
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
Graveyard Cave
Mimic Cave
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
Overworld Entrance naming:
Links House: The hero's old residence
Turtle Rock: Turtle Rock Main
Misery Mire: Misery Mire
Ice Palace: Ice Palace
Skull Woods Final Section: The back of Skull Woods
Death Mountain Return Cave (West): The SW DM Foothills Cave
Mimic Cave: Mimic Ledge
Hammer Peg Cave: The rows of pegs
Pyramid Fairy: The crack on the pyramid
Eastern Palace: Eastern Palace
Elder House (East): Elder House
Elder House (West): Elder House
Two Brothers House (East): Eastern Quarreling Brothers' house
Old Man Cave (West): The lower DM entrance
Hyrule Castle Entrance (South): The ground level castle door
Thieves Town: Thieves' Town
Bumper Cave (Bottom): The lower Bumper Cave
Swamp Palace: Swamp Palace
Dark Death Mountain Ledge (West): The East dark DM connector ledge
Dark Death Mountain Ledge (East): The East dark DM connector ledge
Superbunny Cave (Top): The summit of dark DM cave
Superbunny Cave (Bottom): The base of east dark DM
Hookshot Cave: The rock on dark DM
Desert Palace Entrance (South): The book sealed passage
Tower of Hera: The Tower of Hera
Two Brothers House (West): The door near the race game
Old Man Cave (East): The SW-most cave on west DM
Old Man House (Bottom): A cave with a door on west DM
Old Man House (Top): The eastmost cave on west DM
Death Mountain Return Cave (East): The westmost cave on west DM
Spectacle Rock Cave Peak: The highest cave on west DM
Spectacle Rock Cave: The right ledge on west DM
Spectacle Rock Cave (Bottom): The left ledge on west DM
Paradox Cave (Bottom): The right paired cave on east DM
Paradox Cave (Middle): The southmost cave on east DM
Paradox Cave (Top): The east DM summit cave
Fairy Ascension Cave (Bottom): The east DM cave behind rocks
Fairy Ascension Cave (Top): The central ledge on east DM
Spiral Cave: The left ledge on east DM
Spiral Cave (Bottom): The SWmost cave on east DM
Palace of Darkness: Palace of Darkness
Hyrule Castle Entrance (West): The left castle door
Hyrule Castle Entrance (East): The right castle door
Agahnims Tower: The sealed castle door
Desert Palace Entrance (West): The westmost building in the desert
Desert Palace Entrance (North): The northmost cave in the desert
Blinds Hideout: Blind's old house
Lake Hylia Fairy: A cave NE of Lake Hylia
Light Hype Fairy: The cave south of your house
Desert Fairy: The cave near the desert
Chicken House: The chicken lady's house
Tavern North: A backdoor
Aginahs Cave: The open desert cave
Sahasrahlas Hut: The house near armos
Lake Hylia Shop: The cave NW Lake Hylia
Blacksmiths Hut: The old smithery
Sick Kids House: The central house in Kakariko
Lost Woods Gamble: A tree trunk door
Fortune Teller (Light): A building NE of Kakariko
Snitch Lady (East): A house guarded by a snitch
Snitch Lady (West): A house guarded by a snitch
Bush Covered House: A house with an uncut lawn
Tavern (Front): A building with a backdoor
Light World Bomb Hut: A Kakariko building with no door
Kakariko Shop: The old Kakariko shop
Mini Moldorm Cave: The cave south of Lake Hylia
Long Fairy Cave: The eastmost portal cave
Good Bee Cave: The open cave SE Lake Hylia
20 Rupee Cave: The rock SE Lake Hylia
50 Rupee Cave: The rock near the desert
Ice Rod Cave: The sealed cave SE Lake Hylia
Library: The old library
Potion Shop: The witch's building
Dam: The old dam
Lumberjack House: The lumberjack house
Lake Hylia Fortune Teller: The building NW Lake Hylia
Kakariko Gamble Game: The old Kakariko gambling den
Waterfall of Wishing: Going behind the waterfall
Capacity Upgrade: The cave on the island
Bonk Rock Cave: The rock pile near Sanctuary
Graveyard Cave: The graveyard ledge
Checkerboard Cave: The NE desert ledge
Cave 45: The ledge south of haunted grove
Kings Grave: The northeastmost grave
Bonk Fairy (Light): The rock pile near your home
Hookshot Fairy: A cave on east DM
Bonk Fairy (Dark): The rock pile near the old bomb shop
Dark Sanctuary Hint: The dark sanctuary cave
Dark Lake Hylia Fairy: The cave NE dark Lake Hylia
C-Shaped House: The NE house in Village of Outcasts
Big Bomb Shop: The old bomb shop
Dark Death Mountain Fairy: The SW cave on dark DM
Dark Lake Hylia Shop: The building NW dark Lake Hylia
Dark World Shop: The hammer sealed building
Red Shield Shop: The fenced in building
Mire Shed: The western hut in the mire
East Dark World Hint: The dark cave near the eastmost portal
Mire Hint: The cave east of the mire
Spike Cave: The ledge cave on west dark DM
Palace of Darkness Hint: The building south of Kiki
Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia
Dark Death Mountain Shop: The base of east dark DM
Dark Potion Shop: The building near the catfish
Archery Game: The old archery game
Dark Lumberjack Shop: The northmost Dark World building
Hype Cave: The cave south of the old bomb shop
Brewery: The Village of Outcasts building with no door
Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia
Chest Game: The westmost building in the Village of Outcasts
Mire Fairy: The eastern hut in the mire
Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia
Fortune Teller (Dark): The building NE the Village of Outcasts
Sanctuary: Sanctuary
Lumberjack Tree Cave: The cave Behind Lumberjacks
Lost Woods Hideout Stump: The stump in Lost Woods
North Fairy Cave: The cave East of Graveyard
Bat Cave Cave: The cave in eastern Kakariko
Kakariko Well Cave: The cave in northern Kakariko
Hyrule Castle Secret Entrance Stairs: The tunnel near the castle
Skull Woods First Section Door: The southeastmost skull
Skull Woods Second Section Door (East): The central open skull
Skull Woods Second Section Door (West): The westmost open skull
Desert Palace Entrance (East): The eastern building in the desert
Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM
Bumper Cave (Top): The upper Bumper Cave
Hookshot Cave Back Entrance: The stairs on the floating island
Destination Entrance Naming:
Hyrule Castle: Hyrule Castle (all three entrances)
Eastern Palace: Eastern Palace
Desert Palace: Desert Palace (all four entrances, including final)
Tower of Hera: Tower of Hera
Palace of Darkness: Palace of Darkness
Swamp Palace: Swamp Palace
Skull Woods: Skull Woods (any entrance including final)
Thieves' Town: Thieves' Town
Ice Palace: Ice Palace
Misery Mire: Misery Mire
Turtle Rock: Turtle Rock (all four entrances)
Ganon's Tower: Ganon's Tower
Castle Tower: Agahnim's Tower
A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave
A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout
Sahasrahla: Sahasrahla
A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy
A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave
A common shop: Any shop that sells bombs by default
The rare shop: The shop that sells the Red Shield by default
The potion shop: Potion Shop
The bomb shop: Bomb Shop
A fortune teller: Any of the three fortune tellers
A house with a chest: Chicken Lady's house, C-House, Brewery
A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave
A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave
The dam: Watergate
The sick kid: Sick Kid
The library: Library
Mimic Cave: Mimic Cave
Spike Cave: Spike Cave
A game of 16 chests: VoO chest game (for the item)
A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint
A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots)
A game of chance: Gambling game (just for cash, no items)
A game of skill: Archery minigame
The queen of fairies: Capacity Upgrade Fairy
A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit
A restock room: The Kakariko bomb/arrow restock room
The tavern: The Kakariko tavern
The grass man: The Kakariko man with many beds
A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee
Fairies deep in a cave: Hookshot Fairy
Location naming reference:
Mushroom: in the woods
Master Sword Pedestal: at the pedestal
Bottle Merchant: with a merchant
Stumpy: with tree boy
Flute Spot: underground
Digging Game: underground
Lake Hylia Island: on an island
Floating Island: on an island
Bumper Cave Ledge: on a ledge
Spectacle Rock: atop a rock
Maze Race: at the race
Desert Ledge: in the desert
Pyramid: on the pyramid
Catfish: with a catfish
Ether Tablet: at a monument
Bombos Tablet: at a monument
Hobo: with the hobo
Zora's Ledge: near Zora
King Zora: at a high price
Sunken Treasure: underwater
Floodgate Chest: in the dam
Blacksmith: with the smith
Purple Chest: from a box
Old Man: with the old man
Link's Uncle: with your uncle
Secret Passage: near your uncle
Kakariko Well (5 items): in a well
Lost Woods Hideout: near a thief
Lumberjack Tree: in a hole
Magic Bat: with the bat
Paradox Cave (7 items): in a cave with seven chests
Blind's Hideout (5 items): in a basement
Mini Moldorm Cave (5 items): near Moldorms
Hype Cave (4 back chests): near a bat-like man
Hype Cave - Generous Guy: with a bat-like man
Hookshot Cave (4 items): across pits
Sahasrahla's Hut (chests in back): near the elder
Sahasrahla: with the elder
Waterfall Fairy (2 items): near a fairy
Pyramid Fairy (2 items): near a fairy
Mire Shed (2 items): near sparks
Superbunny Cave (2 items): in a connection
Spiral Cave: in spiral cave
Kakariko Tavern: in the bar
Link's House: in your home
Sick Kid: with the sick
Library: near books
Potion Shop: near potions
Spike Cave: beyond spikes
Mimic Cave: in a cave of mimicry
Chest Game: as a game reward
Chicken House: near poultry
Aginah's Cave: with Aginah
Ice Rod Cave: in a frozen cave
Brewery: alone in a home
C-Shaped House: alone in a home
Spectacle Rock Cave: alone in a cave
King's Tomb: alone in a cave
Cave 45: alone in a cave
Graveyard Cave: alone in a cave
Checkerboard Cave: alone in a cave
Bonk Rock Cave: alone in a cave
Peg Cave: alone in a cave
Sanctuary: in Sanctuary
Hyrule Castle - Boomerang Chest: in Hyrule Castle
Hyrule Castle - Map Chest: in Hyrule Castle
Hyrule Castle - Zelda's Chest: in Hyrule Castle
Sewers - Dark Cross: in the sewers
Sewers - Secret Room (3 items): in the sewers
Eastern Palace - Boss: with the Armos
Eastern Palace (otherwise, 5 items): in Eastern Palace
Desert Palace - Boss: with Lanmolas
Desert Palace (otherwise, 5 items): in Desert Palace
Tower of Hera - Boss: with Moldorm
Tower of Hera (otherwise, 5 items): in Tower of Hera
Castle Tower (2 items): in Castle Tower
Palace of Darkness - Boss: with Helmasaur King
Palace of Darkness (otherwise, 13 items): in Palace of Darkness
Swamp Palace - Boss: with Arrghus
Swamp Palace (otherwise, 9 items): in Swamp Palace
Skull Woods - Bridge Room: near Mothula
Skull Woods - Boss: with Mothula
Skull Woods (otherwise, 6 items): in Skull Woods
Thieves' Town - Boss: with Blind
Thieves' Town (otherwise, 7 items): in Thieves' Town
Ice Palace - Boss: with Kholdstare
Ice Palace (otherwise, 7 items): in Ice Palace
Misery Mire - Boss: with Vitreous
Misery Mire (otherwise, 7 items): in Misery Mire
Turtle Rock - Boss: with Trinexx
Turtle Rock (otherwise, 11 items): in Turtle Rock
Ganons Tower (after climb, 4 items): atop Ganon's Tower
Hint description:
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
4 hints for inconvenient entrances.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
4 junk hints.
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
5 hints for inconvenient item locations.
8 hints for valuable items.
7 junk hints.
In the simple, restricted shuffles, these are the ratios:
2 hints for inconvenient entrances.
1 hint for an inconvenient dungeon entrance.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
5 junk hints.
These hints will use the following format:
Entrance hints go "[Entrance on overworld] leads to [interior]".
Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass).
The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots.
Hint Locations:
Eastern Palace room before Big Chest
Desert Palace bonk torch room
Tower of Hera entrance room
Tower of Hera Big Chest room
Castle Tower after dark rooms
Palace of Darkness before Bow section
Swamp Palace entryway
Thieves' Town upstairs
Ice Palace entrance
Ice Palace after first drop
Ice Palace tall ice floor room
Misery Mire cutscene room
Turtle Rock entrance
Spectacle Rock cave
Spiky Hint cave
PoD Bdlg NPC
Near PoD Storyteller (bug near bomb wall)
Dark Sanctuary Storyteller (long room with tables)
Near Mire Storyteller (feather duster in winding cave)
SE DW Storyteller (owl in winding cave)
Inconvenient entrance list:
Skull Woods Final
Ice Palace
Misery Mire
Turtle Rock
Ganon's Tower
Mimic Ledge
SW DM Foothills Cave (mirror from upper Bumper ledge)
Hammer Pegs (near purple chest)
Super Bomb cracked wall
Inconvenient location list:
Swamp left (two chests)
Mire left (two chests)
Hera basement
Eastern Palace Big Key chest (protected by anti-fairies)
Thieves' Town Big Chest
Ice Palace Big Chest
Ganon's Tower Big Chest
Purple Chest
Spike Cave
Magic Bat
Sahasrahla (Green Pendant)
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
Graveyard Cave
Mimic Cave
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
Overworld Entrance naming:
Links House: The hero's old residence
Turtle Rock: Turtle Rock Main
Misery Mire: Misery Mire
Ice Palace: Ice Palace
Skull Woods Final Section: The back of Skull Woods
Death Mountain Return Cave (West): The SW DM Foothills Cave
Mimic Cave: Mimic Ledge
Hammer Peg Cave: The rows of pegs
Pyramid Fairy: The crack on the pyramid
Eastern Palace: Eastern Palace
Elder House (East): Elder House
Elder House (West): Elder House
Two Brothers House (East): Eastern Quarreling Brothers' house
Old Man Cave (West): The lower DM entrance
Hyrule Castle Entrance (South): The ground level castle door
Thieves Town: Thieves' Town
Bumper Cave (Bottom): The lower Bumper Cave
Swamp Palace: Swamp Palace
Dark Death Mountain Ledge (West): The East dark DM connector ledge
Dark Death Mountain Ledge (East): The East dark DM connector ledge
Superbunny Cave (Top): The summit of dark DM cave
Superbunny Cave (Bottom): The base of east dark DM
Hookshot Cave: The rock on dark DM
Desert Palace Entrance (South): The book sealed passage
Tower of Hera: The Tower of Hera
Two Brothers House (West): The door near the race game
Old Man Cave (East): The SW-most cave on west DM
Old Man House (Bottom): A cave with a door on west DM
Old Man House (Top): The eastmost cave on west DM
Death Mountain Return Cave (East): The westmost cave on west DM
Spectacle Rock Cave Peak: The highest cave on west DM
Spectacle Rock Cave: The right ledge on west DM
Spectacle Rock Cave (Bottom): The left ledge on west DM
Paradox Cave (Bottom): The right paired cave on east DM
Paradox Cave (Middle): The southmost cave on east DM
Paradox Cave (Top): The east DM summit cave
Fairy Ascension Cave (Bottom): The east DM cave behind rocks
Fairy Ascension Cave (Top): The central ledge on east DM
Spiral Cave: The left ledge on east DM
Spiral Cave (Bottom): The SWmost cave on east DM
Palace of Darkness: Palace of Darkness
Hyrule Castle Entrance (West): The left castle door
Hyrule Castle Entrance (East): The right castle door
Agahnims Tower: The sealed castle door
Desert Palace Entrance (West): The westmost building in the desert
Desert Palace Entrance (North): The northmost cave in the desert
Blinds Hideout: Blind's old house
Lake Hylia Fairy: A cave NE of Lake Hylia
Light Hype Fairy: The cave south of your house
Desert Fairy: The cave near the desert
Chicken House: The chicken lady's house
Tavern North: A backdoor
Aginahs Cave: The open desert cave
Sahasrahlas Hut: The house near armos
Lake Hylia Shop: The cave NW Lake Hylia
Blacksmiths Hut: The old smithery
Sick Kids House: The central house in Kakariko
Lost Woods Gamble: A tree trunk door
Fortune Teller (Light): A building NE of Kakariko
Snitch Lady (East): A house guarded by a snitch
Snitch Lady (West): A house guarded by a snitch
Bush Covered House: A house with an uncut lawn
Tavern (Front): A building with a backdoor
Light World Bomb Hut: A Kakariko building with no door
Kakariko Shop: The old Kakariko shop
Mini Moldorm Cave: The cave south of Lake Hylia
Long Fairy Cave: The eastmost portal cave
Good Bee Cave: The open cave SE Lake Hylia
20 Rupee Cave: The rock SE Lake Hylia
50 Rupee Cave: The rock near the desert
Ice Rod Cave: The sealed cave SE Lake Hylia
Library: The old library
Potion Shop: The witch's building
Dam: The old dam
Lumberjack House: The lumberjack house
Lake Hylia Fortune Teller: The building NW Lake Hylia
Kakariko Gamble Game: The old Kakariko gambling den
Waterfall of Wishing: Going behind the waterfall
Capacity Upgrade: The cave on the island
Bonk Rock Cave: The rock pile near Sanctuary
Graveyard Cave: The graveyard ledge
Checkerboard Cave: The NE desert ledge
Cave 45: The ledge south of haunted grove
Kings Grave: The northeastmost grave
Bonk Fairy (Light): The rock pile near your home
Hookshot Fairy: A cave on east DM
Bonk Fairy (Dark): The rock pile near the old bomb shop
Dark Sanctuary Hint: The dark sanctuary cave
Dark Lake Hylia Fairy: The cave NE dark Lake Hylia
C-Shaped House: The NE house in Village of Outcasts
Big Bomb Shop: The old bomb shop
Dark Death Mountain Fairy: The SW cave on dark DM
Dark Lake Hylia Shop: The building NW dark Lake Hylia
Dark World Shop: The hammer sealed building
Red Shield Shop: The fenced in building
Mire Shed: The western hut in the mire
East Dark World Hint: The dark cave near the eastmost portal
Mire Hint: The cave east of the mire
Spike Cave: The ledge cave on west dark DM
Palace of Darkness Hint: The building south of Kiki
Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia
Dark Death Mountain Shop: The base of east dark DM
Dark Potion Shop: The building near the catfish
Archery Game: The old archery game
Dark Lumberjack Shop: The northmost Dark World building
Hype Cave: The cave south of the old bomb shop
Brewery: The Village of Outcasts building with no door
Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia
Chest Game: The westmost building in the Village of Outcasts
Mire Fairy: The eastern hut in the mire
Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia
Fortune Teller (Dark): The building NE the Village of Outcasts
Sanctuary: Sanctuary
Lumberjack Tree Cave: The cave Behind Lumberjacks
Lost Woods Hideout Stump: The stump in Lost Woods
North Fairy Cave: The cave East of Graveyard
Bat Cave Cave: The cave in eastern Kakariko
Kakariko Well Cave: The cave in northern Kakariko
Hyrule Castle Secret Entrance Stairs: The tunnel near the castle
Skull Woods First Section Door: The southeastmost skull
Skull Woods Second Section Door (East): The central open skull
Skull Woods Second Section Door (West): The westmost open skull
Desert Palace Entrance (East): The eastern building in the desert
Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM
Bumper Cave (Top): The upper Bumper Cave
Hookshot Cave Back Entrance: The stairs on the floating island
Destination Entrance Naming:
Hyrule Castle: Hyrule Castle (all three entrances)
Eastern Palace: Eastern Palace
Desert Palace: Desert Palace (all four entrances, including final)
Tower of Hera: Tower of Hera
Palace of Darkness: Palace of Darkness
Swamp Palace: Swamp Palace
Skull Woods: Skull Woods (any entrance including final)
Thieves' Town: Thieves' Town
Ice Palace: Ice Palace
Misery Mire: Misery Mire
Turtle Rock: Turtle Rock (all four entrances)
Ganon's Tower: Ganon's Tower
Castle Tower: Agahnim's Tower
A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave
A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout
Sahasrahla: Sahasrahla
A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy
A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave
A common shop: Any shop that sells bombs by default
The rare shop: The shop that sells the Red Shield by default
The potion shop: Potion Shop
The bomb shop: Bomb Shop
A fortune teller: Any of the three fortune tellers
A house with a chest: Chicken Lady's house, C-House, Brewery
A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave
A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave
The dam: Watergate
The sick kid: Sick Kid
The library: Library
Mimic Cave: Mimic Cave
Spike Cave: Spike Cave
A game of 16 chests: VoO chest game (for the item)
A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint
A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots)
A game of chance: Gambling game (just for cash, no items)
A game of skill: Archery minigame
The queen of fairies: Capacity Upgrade Fairy
A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit
A restock room: The Kakariko bomb/arrow restock room
The tavern: The Kakariko tavern
The grass man: The Kakariko man with many beds
A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee
Fairies deep in a cave: Hookshot Fairy
Location naming reference:
Mushroom: in the woods
Master Sword Pedestal: at the pedestal
Bottle Merchant: with a merchant
Stumpy: with tree boy
Flute Spot: underground
Digging Game: underground
Lake Hylia Island: on an island
Floating Island: on an island
Bumper Cave Ledge: on a ledge
Spectacle Rock: atop a rock
Maze Race: at the race
Desert Ledge: in the desert
Pyramid: on the pyramid
Catfish: with a catfish
Ether Tablet: at a monument
Bombos Tablet: at a monument
Hobo: with the hobo
Zora's Ledge: near Zora
King Zora: at a high price
Sunken Treasure: underwater
Floodgate Chest: in the dam
Blacksmith: with the smith
Purple Chest: from a box
Old Man: with the old man
Link's Uncle: with your uncle
Secret Passage: near your uncle
Kakariko Well (5 items): in a well
Lost Woods Hideout: near a thief
Lumberjack Tree: in a hole
Magic Bat: with the bat
Paradox Cave (7 items): in a cave with seven chests
Blind's Hideout (5 items): in a basement
Mini Moldorm Cave (5 items): near Moldorms
Hype Cave (4 back chests): near a bat-like man
Hype Cave - Generous Guy: with a bat-like man
Hookshot Cave (4 items): across pits
Sahasrahla's Hut (chests in back): near the elder
Sahasrahla: with the elder
Waterfall Fairy (2 items): near a fairy
Pyramid Fairy (2 items): near a fairy
Mire Shed (2 items): near sparks
Superbunny Cave (2 items): in a connection
Spiral Cave: in spiral cave
Kakariko Tavern: in the bar
Link's House: in your home
Sick Kid: with the sick
Library: near books
Potion Shop: near potions
Spike Cave: beyond spikes
Mimic Cave: in a cave of mimicry
Chest Game: as a game reward
Chicken House: near poultry
Aginah's Cave: with Aginah
Ice Rod Cave: in a frozen cave
Brewery: alone in a home
C-Shaped House: alone in a home
Spectacle Rock Cave: alone in a cave
King's Tomb: alone in a cave
Cave 45: alone in a cave
Graveyard Cave: alone in a cave
Checkerboard Cave: alone in a cave
Bonk Rock Cave: alone in a cave
Peg Cave: alone in a cave
Sanctuary: in Sanctuary
Hyrule Castle - Boomerang Chest: in Hyrule Castle
Hyrule Castle - Map Chest: in Hyrule Castle
Hyrule Castle - Zelda's Chest: in Hyrule Castle
Sewers - Dark Cross: in the sewers
Sewers - Secret Room (3 items): in the sewers
Eastern Palace - Boss: with the Armos
Eastern Palace (otherwise, 5 items): in Eastern Palace
Desert Palace - Boss: with Lanmolas
Desert Palace (otherwise, 5 items): in Desert Palace
Tower of Hera - Boss: with Moldorm
Tower of Hera (otherwise, 5 items): in Tower of Hera
Castle Tower (2 items): in Castle Tower
Palace of Darkness - Boss: with Helmasaur King
Palace of Darkness (otherwise, 13 items): in Palace of Darkness
Swamp Palace - Boss: with Arrghus
Swamp Palace (otherwise, 9 items): in Swamp Palace
Skull Woods - Bridge Room: near Mothula
Skull Woods - Boss: with Mothula
Skull Woods (otherwise, 6 items): in Skull Woods
Thieves' Town - Boss: with Blind
Thieves' Town (otherwise, 7 items): in Thieves' Town
Ice Palace - Boss: with Kholdstare
Ice Palace (otherwise, 7 items): in Ice Palace
Misery Mire - Boss: with Vitreous
Misery Mire (otherwise, 7 items): in Misery Mire
Turtle Rock - Boss: with Trinexx
Turtle Rock (otherwise, 11 items): in Turtle Rock
Ganons Tower (after climb, 4 items): atop Ganon's Tower
Ganon's Tower (otherwise, 23 items): in Ganon's Tower

View File

@@ -799,7 +799,7 @@ def sell_potions(world, player):
if shop.region.name in shop_to_location_table and shop.region.name != 'Capacity Upgrade':
loc_choices += [world.get_location(loc, player) for loc in shop_to_location_table[shop.region.name]]
locations = [loc for loc in loc_choices if not loc.item]
for potion in ['Green Potion', 'Blue Potion', 'Red Potion']:
for potion in ['Green Potion', 'Blue Potion', 'Red Potion', 'Bee']:
location = random.choice(filter_locations(ItemFactory(potion, player), locations, world, potion=True))
locations.remove(location)
p_item = next((item for item in world.itempool if item.name == potion and item.player == player), None)
@@ -1293,4 +1293,4 @@ def set_prize_drops(world, player):
# saved fish prize
world.prizes[player]['fish'] = prizes.pop()
world.prizes[player]['enemies'] = prizes
world.prizes[player]['enemies'] = prizes

7
Gui.py
View File

@@ -22,8 +22,6 @@ from tkinter import (
from CLI import get_args_priority
from DungeonRandomizer import parse_cli
from GuiUtils import set_icon
from Main import __version__ as ESVersion
from OverworldShuffle import __version__ as ORVersion
from source.classes.BabelFish import BabelFish
from source.classes.Empty import Empty
from source.gui.adjust.overview import adjust_page
@@ -40,13 +38,14 @@ from source.gui.randomize.generation import generation_page
from source.gui.randomize.item import item_page
from source.gui.randomize.overworld import overworld_page
from source.gui.startinventory.overview import startinventory_page
from Versions import DRVersion, GKVersion, ORVersion
def check_python_version(fish):
import sys
version = sys.version_info
if version.major < 3 or version.minor < 7:
messagebox.showinfo("Overworld Shuffle %s (DR %s)" % (ORVersion, ESVersion), fish.translate("cli","cli","old.python.version") % sys.version)
messagebox.showinfo("GwaaKiwi Randomizer %s (OR %s, DR %s)" % (GKVersion, ORVersion, DRVersion), fish.translate("cli","cli","old.python.version") % sys.version)
# Save settings to file
@@ -95,7 +94,7 @@ def guiMain(args=None):
mainWindow = Tk()
self = mainWindow
mainWindow.wm_title("Overworld Shuffle %s (DR %s)" % (ORVersion, ESVersion))
mainWindow.wm_title("GwaaKiwi Randomizer %s (OR %s, DR %s)" % (GKVersion, ORVersion, DRVersion))
mainWindow.protocol("WM_DELETE_WINDOW", guiExit) # intercept when user clicks the X
# set program icon

View File

@@ -236,12 +236,14 @@ def get_custom_array_key(item):
def generate_itempool(world, player):
if (world.difficulty[player] not in ['normal', 'hard', 'expert']
or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals',
'ganonhunt', 'completionist', 'sanctuary']
or world.goal[player] not in ['ganon', 'pedestal', 'dungeons',
'triforcehunt', 'trinity', 'crystals',
'ganonhunt', 'completionist', 'sanctuary',
'bosshunt']
or world.mode[player] not in ['open', 'standard', 'inverted']
or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']
or world.progressive not in ['on', 'off', 'random']):
raise NotImplementedError('Not supported yet')
raise NotImplementedError('Not supported yet')
if world.timer in ['ohko', 'timed-ohko']:
world.can_take_damage[player] = False
@@ -379,7 +381,7 @@ def generate_itempool(world, player):
items = ItemFactory(pool, player)
if world.shopsanity[player]:
for potion in ['Green Potion', 'Blue Potion', 'Red Potion']:
for potion in ['Green Potion', 'Blue Potion', 'Red Potion', 'Bee']:
p_item = next(item for item in items if item.name == potion and item.player == player)
p_item.priority = True # don't beemize one of each potion
@@ -1125,7 +1127,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots')
pool.extend(['Rupees (20)'])
if want_progressives():
pool.extend(progressivegloves)
else:
@@ -1520,7 +1522,7 @@ def make_customizer_pool(world, player):
guaranteed_items.append('Ocarina (Activated)')
missing_items = []
if world.shopsanity[player]:
guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion'])
guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion', 'Bee'])
if world.keyshuffle[player] == 'universal':
guaranteed_items.append('Small Key (Universal)')
for item in guaranteed_items:

44
Main.py
View File

@@ -74,6 +74,7 @@ from Rom import (
)
from RoomData import create_rooms
from Rules import set_rules
from source.classes.BabelFish import BabelFish
from source.classes.CustomSettings import CustomSettings
from source.enemizer.DamageTables import DamageTable
from source.enemizer.Enemizer import randomize_enemies
@@ -92,12 +93,7 @@ from UnderworldGlitchRules import (
create_hmg_entrances_regions,
)
from Utils import output_path, parse_player_names
version_number = '1.5.0'
version_branch = '-u'
__version__ = f'{version_number}{version_branch}'
from source.classes.BabelFish import BabelFish
from Versions import DRVersion, GKVersion, ORVersion
class EnemizerError(RuntimeError):
@@ -133,7 +129,7 @@ def random_ganon_item(sword_mode):
def main(args, seed=None, fish=None):
check_python_version()
if args.print_template_yaml:
return export_yaml(args, fish)
@@ -175,16 +171,14 @@ def main(args, seed=None, fish=None):
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
world.finish_init()
from OverworldShuffle import __version__ as ORVersion
logger.info(
world.fish.translate("cli","cli","app.title") + "\n",
ORVersion,
"%s (%s)" % (world.seed, str(args.outputname)) if str(args.outputname).startswith('M') else world.seed,
Settings.make_code(world, 1) if world.players == 1 else ''
world.fish.translate("cli","cli","app.title") + "\n",
GKVersion,
"%s (%s)" % (world.seed, str(args.outputname)) if str(args.outputname).startswith('M') else world.seed,
)
for k,v in {"DR":__version__,"OR":ORVersion}.items():
logger.info((k + ' Version:').ljust(16) + '%s' % v)
for k,v in {"GK": GKVersion, "OR": ORVersion, "DR": DRVersion}.items():
logger.info((k + ' Version:').ljust(16) + '%s' % v)
parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names)
@@ -195,7 +189,7 @@ def main(args, seed=None, fish=None):
world.player_names[player].append(name)
logger.info('')
outfilebase = f'OR_{args.outputname if args.outputname else world.seed}'
outfilebase = f'GK_{args.outputname if args.outputname else world.seed}'
for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
@@ -474,15 +468,13 @@ def export_yaml(args, fish):
if args.seed and int(args.seed) > 0:
world.seed = int(args.seed)
from OverworldShuffle import __version__ as ORVersion
logger.info(
world.fish.translate("cli","cli","app.title") + "\n",
ORVersion,
GKVersion,
"(%s)" % outfilebase,
Settings.make_code(world, 1) if world.players == 1 else ''
)
for k,v in {"DR":__version__,"OR":ORVersion}.items():
for k,v in {"GK": GKVersion, "OR": ORVersion, "DR": DRVersion}.items():
logger.info((k + ' Version:').ljust(16) + '%s' % v)
for player in range(1, world.players + 1):
@@ -523,12 +515,16 @@ def init_world(args, fish):
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.prizeshuffle = args.prizeshuffle.copy()
world.showloot = args.showloot.copy()
world.showmap = args.showmap.copy()
world.bombbag = args.bombbag.copy()
world.flute_mode = args.flute_mode.copy()
world.bow_mode = args.bow_mode.copy()
world.crystals_ganon_orig = args.crystals_ganon.copy()
world.crystals_gt_orig = args.crystals_gt.copy()
world.ganon_item_orig = args.ganon_item.copy()
world.bosses_ganon = {player: int(args.bosses_ganon[player]) for player in range(1, world.players + 1)}
world.bosshunt_include_agas = args.bosshunt_include_agas.copy()
world.owTerrain = args.ow_terrain.copy()
world.owKeepSimilar = args.ow_keepsimilar.copy()
world.owWhirlpoolShuffle = args.ow_whirlpool.copy()
@@ -576,7 +572,7 @@ def init_world(args, fish):
world.money_balance = args.money_balance.copy()
# custom settings - these haven't been promoted to full settings yet
in_progress_settings = ['force_enemy', 'free_lamp_cone']
in_progress_settings = ['force_enemy']
for player in range(1, world.players + 1):
for setting in in_progress_settings:
if world.customizer and world.customizer.has_setting(player, setting):
@@ -838,12 +834,16 @@ def copy_world(world):
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.prizeshuffle = world.prizeshuffle.copy()
ret.showloot = world.showloot.copy()
ret.showmap = world.showmap.copy()
ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy()
ret.bow_mode = world.bow_mode.copy()
ret.free_lamp_cone = world.free_lamp_cone.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.bosses_ganon = world.bosses_ganon.copy()
ret.bosshunt_include_agas = world.bosshunt_include_agas.copy()
ret.ganon_item = world.ganon_item.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
ret.crystals_gt_orig = world.crystals_gt_orig.copy()
@@ -1065,12 +1065,16 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.prizeshuffle = world.prizeshuffle.copy()
ret.showloot = world.showloot.copy()
ret.showmap = world.showmap.copy()
ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy()
ret.bow_mode = world.bow_mode.copy()
ret.free_lamp_cone = world.free_lamp_cone.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.bosses_ganon = world.bosses_ganon.copy()
ret.bosshunt_include_agas = world.bosshunt_include_agas.copy()
ret.ganon_item = world.ganon_item.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
ret.crystals_gt_orig = world.crystals_gt_orig.copy()

View File

@@ -27,12 +27,6 @@ from Regions import mark_light_dark_world_regions
from source.overworld.EntranceShuffle2 import connect_simple
from Utils import bidict
version_number = '0.6.1.7'
# branch indicator is intentionally different across branches
version_branch = ''
__version__ = '%s%s' % (version_number, version_branch)
parallel_links_new = None # needs to be globally available, reset every new generation/player
def link_overworld(world, player):

View File

@@ -29,7 +29,7 @@ from source.overworld.EntranceShuffle2 import (
link_entrances_new,
)
__version__ = '0.2-dev'
PlandoVersion = '0.2-dev'
def main(args):
start_time = time.process_time()
@@ -47,7 +47,7 @@ def main(args):
random.seed(world.seed)
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', PlandoVersion, args.plando)
world.difficulty_requirements[1] = difficulties[world.difficulty[1]]

143
Rom.py
View File

@@ -81,9 +81,10 @@ from Text import (
text_addresses,
)
from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc
from Versions import DRVersion, GKVersion, ORVersion
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '76dc2d00e5dd5b925ad01574b327d364'
RANDOMIZERBASEHASH = '2647cc28bca3675152576dd1f5ea0bab'
class JsonRom(object):
@@ -201,11 +202,12 @@ class LocalRom(object):
with open(local_path('data/base2current.bps'), 'rb') as stream:
bps.apply.apply_to_bytearrays(bps.io.read_bps(stream), orig_buffer, self.buffer)
# verify md5
patchedmd5 = hashlib.md5()
patchedmd5.update(self.buffer)
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
if not os.getenv("SKIP_BASEROM_CHECK", False):
# verify md5
patchedmd5 = hashlib.md5()
patchedmd5.update(self.buffer)
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
def write_crc(self):
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
@@ -1225,7 +1227,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
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 triforce pieces (instant win not enabled)
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
@@ -1254,7 +1256,6 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
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_byte(0x50599, 0x00) # disable below ganon chest
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
if world.is_pyramid_open(player):
rom.initial_sram.pre_open_pyramid_hole()
rom.write_byte(0x18008F, 0x01 if world.is_atgt_swapped(player) else 0x00) # AT/GT swapped
@@ -1300,6 +1301,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# 08: Goal items collected (ie. Triforce Pieces)
# 09: Max collection rate
# 0A: Custom goal
# 0B: Reserved for Bingo
# 0C: All bosses (prize bosses + aga1 + aga2)
def get_goal_bytes(type):
goal_bytes = []
@@ -1354,6 +1357,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
ganon_goal += [0x02, world.crystals_needed_for_ganon[player]]
elif world.goal[player] in ['ganonhunt']:
ganon_goal += [0x88] # triforce pieces
elif world.goal[player] in ['bosshunt']:
if world.bosshunt_include_agas[player]:
ganon_goal += [0x0C, world.bosses_ganon[player]] # total bosses
else:
ganon_goal += [0x05, world.bosses_ganon[player]] # prize bosses
elif world.goal[player] in ['completionist']:
ganon_goal += [0x81, 0x82, 0x06, 0x07, 0x89] # AD and max collection rate
else:
@@ -1455,6 +1463,60 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave']):
rom.write_byte(0x18003A, 0x01) # show key counts on map pickup
loot_source = 0x09
if world.prizeshuffle[player] != 'none':
loot_source |= 0x10
if world.pottery[player] not in ['none', 'cave']:
loot_source |= 0x02
if world.dropshuffle[player] != 'none':
loot_source |= 0x04
rom.write_byte(0x1CFF10, loot_source)
if world.showloot[player] == 'never':
rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF11, 0x00)
elif world.showloot[player] == 'presence':
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF11, 0x00)
elif world.showloot[player] == 'compass':
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00])
rom.write_byte(0x1CFF11, 0x01)
elif world.showloot[player] == 'always':
rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF11, 0x00)
if world.showmap[player] == 'visited':
rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05])
elif world.showmap[player] == 'map':
rom.write_bytes(0x1CFF00, [0x01, 0x05, 0x00, 0x05])
elif world.showmap[player] == 'always':
rom.write_bytes(0x1CFF00, [0x05, 0x00, 0x00, 0x00])
loot_icons = 0x1CF900
if world.bombbag[player]:
rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major
triforce_piece_ids = [0x6B, 0x6C]
if world.treasure_hunt_count[player] > 20:
for triforce_piece_id in triforce_piece_ids:
rom.write_byte(loot_icons + triforce_piece_id, 0x04)
crystal_ids = [0x20, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6]
if world.goal[player] in ['ganon', 'dungeons', 'crystals', 'trinity']:
crystal_category = 0x0D
else:
crystal_category = 0x06
for crystal_id in crystal_ids:
rom.write_byte(loot_icons + crystal_id, crystal_category)
pendant_ids = [0x37, 0x38, 0x39]
if world.goal[player] in ['pedestal', 'dungeons', 'trinity']:
pendant_category = 0x0C
else:
pendant_category = 0x06
for pendant_id in pendant_ids:
rom.write_byte(loot_icons + pendant_id, pendant_category)
# compasses showing dungeon count
compass_mode = 0x80 if world.compassshuffle[player] not in ['none', 'nearby'] else 0x00
if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off':
@@ -1601,12 +1663,28 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# b - Big Key
# a - Small Key
#
dungeon_items_menu = 0x00
if world.doorShuffle[player] not in ['vanilla', 'basic']:
dungeon_items_menu |= 0x0F
if world.keyshuffle[player] not in ['none', 'universal']:
dungeon_items_menu |= 0x01
if world.bigkeyshuffle[player] != 'none':
dungeon_items_menu |= 0x02
enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] not in ['none', 'dungeon', 'nearby']
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00)
| (0x02 if world.bigkeyshuffle[player] != 'none' else 0x00)
| (0x04 if world.mapshuffle[player] != 'none' or enable_menu_map_check else 0x00)
| (0x08 if world.compassshuffle[player] != 'none' else 0x00) # free roaming items in menu
| (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon
if world.mapshuffle[player] != 'none' or enable_menu_map_check:
dungeon_items_menu |= 0x04
if world.compassshuffle[player] != 'none':
dungeon_items_menu |= 0x08
if world.logic[player] == 'nologic' or world.goal[player] == 'bosshunt':
dungeon_items_menu |= 0x10
rom.write_byte(0x180045, dungeon_items_menu)
def get_reveal_bytes(itemName):
if world.prizeshuffle[player] != 'wild':
@@ -1815,30 +1893,29 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# set rom name
# 21 bytes
from Main import __version__
from OverworldShuffle import __version__ as ORVersion
if rom_header:
if len(rom_header) > 21:
raise Exception('ROM header too long. Max 21 bytes, found %d bytes.' % len(rom_header))
elif '|' in rom_header:
gen, seedstring = rom_header.split('|', 1)
gen = f'{gen:<3}'
seedstring = f'{int(seedstring):09}' if seedstring.isdigit() else seedstring[:9]
rom.name = bytearray(f'OR{gen}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21]
elif len(rom_header) <= 9:
seedstring = f'{int(rom_header):09}' if rom_header.isdigit() else rom_header
rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21]
else:
rom.name = bytearray(rom_header, 'utf8')[:21]
else:
seedstring = f'{world.seed:09}' if isinstance(world.seed, int) else world.seed
rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21]
if world.players > 1 and len(rom_header) <= 12:
rom.name = bytearray(f"GK_{team + 1}_{player}_{rom_header}", 'utf8')
elif len(rom_header) <= 18:
rom.name = bytearray(f"GK_{rom_header}", 'utf8')
else:
rom.name = bytearray(rom_header, 'utf8')
else:
if world.players > 1:
rom.name = bytearray(f'GK_{team + 1}_{player}_{world.seed}', 'utf8')
else:
rom.name = bytearray(f'GK_{world.seed}', 'utf8')
rom.name = rom.name[:21]
rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name)
rom.write_bytes(0x138010, bytearray(__version__, 'utf8'))
rom.write_bytes(0x138010, bytearray(DRVersion, 'utf8'))
rom.write_bytes(0x150010, bytearray(ORVersion, 'utf8'))
rom.write_bytes(0x1CEEF0, bytearray(GKVersion, 'utf8'))
# set player names
for p in range(1, min(world.players, 255) + 1):
@@ -2712,12 +2789,18 @@ def write_strings(rom, world, player, team):
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])
elif world.goal[player] == 'ganonhunt':
tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon'
elif world.goal[player] == 'bosshunt':
bosshunt_count = '%d guardian%s of %sdungeons' % \
(world.bosses_ganon[player],
'' if world.bosses_ganon[player] == 1 else 's',
'' if world.bosshunt_include_agas[player] else 'prize ')
tt['sign_ganon'] = 'To beat Ganon you must defeat %s.' % bosshunt_count
elif world.goal[player] == 'completionist':
tt['sign_ganon'] = 'Ganon only respects those who have done everything'
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
def get_custom_goal_text(type):
goal_text = world.custom_goals[player][type]['goaltext']
placeholder_count = goal_text.count('%d')

View File

@@ -90,6 +90,15 @@ def set_rules(world, player):
add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player))
elif world.goal[player] == 'ganonhunt':
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'bosshunt':
if world.bosshunt_include_agas[player]:
add_rule(world.get_location('Ganon', player), lambda state:
state.item_count('Beat Agahnim 1', player) +
state.item_count('Beat Agahnim 2', player) +
state.item_count('Beat Boss', player) >= world.bosses_ganon[player])
else:
add_rule(world.get_location('Ganon', player), lambda state:
state.item_count('Beat Boss', player) >= world.bosses_ganon[player])
elif world.goal[player] == 'completionist':
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))

View File

@@ -1781,7 +1781,7 @@ class TextTable(object):
text['mastersword_pedestal_translated'] = CompressedTextMapper.convert("A test of strength: If you have 3 pendants, I'm yours.")
text['telepathic_tile_spectacle_rock'] = CompressedTextMapper.convert("{NOBORDER}\n{NOBORDER}\nUse the Mirror, or the Hookshot and Hammer, to get to Tower of Hera!")
text['telepathic_tile_swamp_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nDrain the floodgate to raise the water here!")
text['telepathic_tile_thieves_town_upstairs'] = CompressedTextMapper.convert("{NOBORDER}\nBlind hate's bright light.")
text['telepathic_tile_thieves_town_upstairs'] = CompressedTextMapper.convert("{NOBORDER}\nBlind hates bright light.")
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['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.")
@@ -2015,7 +2015,7 @@ class TextTable(object):
text['thief_desert_rupee_cave'] = CompressedTextMapper.convert("So you, like, busted down my door, and are being a jerk by talking to me? Normally I would be angry and make you pay for it, but I bet you're just going to break all my pots and steal my 50 rupees.")
text['thief_ice_rupee_cave'] = CompressedTextMapper.convert("I'm a rupee pot farmer. One day I will take over the world with my skillz. Have you met my brother in the desert? He's way richer than I am.")
text['telepathic_tile_south_east_darkworld_cave'] = CompressedTextMapper.convert("~~ dev cave ~~\n no farming\n required")
text['cukeman'] = CompressedTextMapper.convert("Did you hear that Veetorp beat ajneb174 in a 1 on 1 race at AGDQ?")
text['cukeman'] = CompressedTextMapper.convert("Trans rights!")
text['cukeman_2'] = CompressedTextMapper.convert("You found Shabadoo, huh?\nNiiiiice.")
text['potion_shop_no_cash'] = CompressedTextMapper.convert("Yo! I'm not running a charity here.")
text['kakariko_powdered_chicken'] = CompressedTextMapper.convert("Smallhacker…\n\n\nWas hiding, you found me!\n\n\nOkay, you can leave now.")

View File

@@ -8,6 +8,7 @@ import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from collections import defaultdict
from hashlib import md5
from itertools import count
from math import factorial
from pathlib import Path
@@ -102,31 +103,13 @@ def close_console():
pass
def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', new_rom='working.sfc'):
import hashlib
import json
from collections import OrderedDict
with open(old_rom, 'rb') as stream:
old_rom_data = bytearray(stream.read())
def get_new_romhash(new_rom='working.sfc'):
with open(new_rom, 'rb') as stream:
new_rom_data = bytearray(stream.read())
# extend to 2 mb
old_rom_data.extend(bytearray([0x00] * (2097152 - len(old_rom_data))))
out_data = OrderedDict()
for idx, old in enumerate(old_rom_data):
new = new_rom_data[idx]
if old != new:
out_data[idx] = [int(new)]
for offset in reversed(list(out_data.keys())):
if offset - 1 in out_data:
out_data[offset-1].extend(out_data.pop(offset))
with open('data/base2current.json', 'wt') as outfile:
json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":"))
basemd5 = hashlib.md5()
basemd5 = md5()
basemd5.update(new_rom_data)
return "New Rom Hash: " + basemd5.hexdigest()
return basemd5.hexdigest()
def kth_combination(k, l, r):
@@ -768,18 +751,18 @@ class bidict(dict):
super(bidict, self).__init__(*args, **kwargs)
self.inverse = {}
for key, value in self.items():
self.inverse.setdefault(value,[]).append(key)
self.inverse.setdefault(value,[]).append(key)
def __setitem__(self, key, value):
if key in self:
self.inverse[self[key]].remove(key)
self.inverse[self[key]].remove(key)
super(bidict, self).__setitem__(key, value)
self.inverse.setdefault(value,[]).append(key)
self.inverse.setdefault(value,[]).append(key)
def __delitem__(self, key):
value = self[key]
self.inverse.setdefault(value,[]).remove(key)
if value in self.inverse and not self.inverse[value]:
if value in self.inverse and not self.inverse[value]:
del self.inverse[value]
super(bidict, self).__delitem__(key)
@@ -787,12 +770,11 @@ class bidict(dict):
class HexInt(int): pass
def hex_representer(dumper, data):
import yaml
return yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#0{4}x}")
if __name__ == '__main__':
print(make_new_base2current())
print("New Rom Hash:", get_new_romhash())
# read_entrance_data(old_rom=sys.argv[1])
# room_palette_data(old_rom=sys.argv[1])
# extract_data_from_us_rom(sys.argv[1])

3
Versions.py Normal file
View File

@@ -0,0 +1,3 @@
GKVersion = '1.0.0'
ORVersion = '0.6.1.7'
DRVersion = '1.5.0-u'

View File

@@ -1,6 +0,0 @@
import os
from OverworldShuffle import __version__ as OWVersion
with(open(os.path.join("resources","app","meta","manifests","app_version.txt"),"w+")) as f:
f.write(OWVersion)

Binary file not shown.

View File

@@ -1,6 +1,6 @@
[project]
name = "alttpr-python"
version = "0.2.0"
version = "1.0.0"
description = "Python ALttP Randomizer"
readme = "README.md"
requires-python = ">=3.7"

View File

@@ -72,6 +72,7 @@
"trinity",
"crystals",
"ganonhunt",
"bosshunt",
"completionist",
"sanctuary"
]
@@ -299,6 +300,27 @@
"random"
]
},
"bosses_ganon": {
"choices": [
"12",
"11",
"10",
"9",
"8",
"7",
"6",
"5",
"4",
"3",
"2",
"1",
"0"
]
},
"bosshunt_include_agas": {
"action": "store_true",
"type": "bool"
},
"crystals_gt": {
"choices": [
"7",
@@ -431,6 +453,21 @@
"wild"
]
},
"showloot": {
"choices": [
"never",
"presence",
"compass",
"always"
]
},
"showmap": {
"choices": [
"visited",
"map",
"always"
]
},
"keysanity": {
"action": "store_true",
"type": "bool",

View File

@@ -2,7 +2,7 @@
"cli": {
"yes": "Yes",
"no": "No",
"app.title": "ALttP Overworld Randomizer Version %s : --seed %s --code %s",
"app.title": "ALttP GwaaKiwi Randomizer Version %s : --seed %s",
"version": "Version",
"seed": "Seed",
"player": "Player",

View File

@@ -1,18 +0,0 @@
import os
from OverworldShuffle import __version__
OWR_VERSION = __version__
def write_appversion():
APP_VERSION = OWR_VERSION
if "-" in APP_VERSION:
APP_VERSION = APP_VERSION[:APP_VERSION.find("-")]
APP_VERSION_FILE = os.path.join(".","resources","app","meta","manifests","app_version.txt")
with open(APP_VERSION_FILE,"w") as f:
f.seek(0)
f.truncate()
f.write(APP_VERSION)
if __name__ == "__main__":
write_appversion()

View File

@@ -9,58 +9,51 @@ except ModuleNotFoundError as e:
pass
import datetime
from Main import __version__
from Versions import DRVersion, GKVersion, ORVersion
DR_VERSION = __version__
from OverworldShuffle import __version__
OWR_VERSION = __version__
PROJECT_NAME = "ALttP Overworld Randomizer"
def diagpad(str):
return str.ljust(len(f"{PROJECT_NAME} Version") + 5,'.')
return str.ljust(40, '.')
def output():
lines = [
f"{PROJECT_NAME} Diagnostics",
"=================================",
diagpad("UTC Time") + str(datetime.datetime.now(datetime.UTC))[:19],
diagpad("ALttP Door Randomizer Version") + DR_VERSION,
diagpad(f"{PROJECT_NAME} Version") + OWR_VERSION,
diagpad("Python Version") + platform.python_version()
]
lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release()))
if hasattr(sys, "executable"):
lines.append(diagpad("Executable") + sys.executable)
lines.append(diagpad("Build Date") + platform.python_build()[1])
lines.append(diagpad("Compiler") + platform.python_compiler())
if hasattr(sys, "api_version"):
lines.append(diagpad("Python API") + str(sys.api_version))
if hasattr(os, "sep"):
lines.append(diagpad("Filepath Separator") + os.sep)
if hasattr(os, "pathsep"):
lines.append(diagpad("Path Env Separator") + os.pathsep)
lines.append("")
lines.append("Packages")
lines.append("--------")
'''
#this breaks when run from the .exe
reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'])
installed_packages = [r.decode() for r in reqs.split()]
for pkg in installed_packages:
pkg = pkg.split("==")
lines.append(diagpad(pkg[0]) + pkg[1])
'''
installed_packages = []
installed_packages = [str(d) for d in pkg_resources.working_set] #this doesn't work from the .exe either, but it doesn't crash the program
installed_packages.sort()
for pkg in installed_packages:
pkg = pkg.split(' ')
lines.append(diagpad(pkg[0]) + pkg[1])
lines = [
"ALttP GwaaKiwi Randomizer Diagnostics",
"=====================================",
diagpad("UTC Time") + str(datetime.datetime.now(datetime.UTC))[:19],
diagpad("ALttP Door Randomizer Version") + DRVersion,
diagpad("ALttP Overworld Randomizer Version") + ORVersion,
diagpad("ALttP GwaaKiwi Randomizer Version") + GKVersion,
diagpad("Python Version") + platform.python_version(),
]
lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release()))
if hasattr(sys, "executable"):
lines.append(diagpad("Executable") + sys.executable)
lines.append(diagpad("Build Date") + platform.python_build()[1])
lines.append(diagpad("Compiler") + platform.python_compiler())
if hasattr(sys, "api_version"):
lines.append(diagpad("Python API") + str(sys.api_version))
if hasattr(os, "sep"):
lines.append(diagpad("Filepath Separator") + os.sep)
if hasattr(os, "pathsep"):
lines.append(diagpad("Path Env Separator") + os.pathsep)
lines.append("")
return lines
lines.append("Packages")
lines.append("--------")
reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'])
installed_packages = [r.decode() for r in reqs.split()]
for pkg in installed_packages:
pkg = pkg.split("==")
lines.append(diagpad(pkg[0]) + pkg[1])
installed_packages = []
installed_packages = [str(d) for d in pkg_resources.working_set]
installed_packages.sort()
for pkg in installed_packages:
pkg = pkg.split(' ')
lines.append(diagpad(pkg[0]) + pkg[1])
return lines
if __name__ == "__main__":
raise AssertionError(f"Called main() on utility library {__file__}")

View File

@@ -471,10 +471,6 @@ vanilla_sheets = [
(0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
(0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00),
(0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x08), (0x5D, 0x49, 0x00, 0x52), (0x55, 0x49, 0x42, 0x43),
(0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50),
(0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x56, 0x57, 0x50), (0x61, 0x62, 0x63, 0x50),
(0x61, 0x62, 0x63, 0x50), (0x61, 0x56, 0x57, 0x50), (0x61, 0x56, 0x63, 0x50), (0x61, 0x56, 0x57, 0x50),
(0x61, 0x56, 0x33, 0x50), (0x61, 0x56, 0x57, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50)
]
required_boss_sheets = {EnemySprite.ArmosKnight: 9, EnemySprite.Lanmolas: 11, EnemySprite.Moldorm: 12,

View File

@@ -20,8 +20,8 @@ from tkinter import (
import source.classes.diags as diagnostics
import source.gui.widgets as widgets
from Main import __version__
from source.classes.Empty import Empty
from Versions import DRVersion
def generation_page(parent,settings):
@@ -165,9 +165,9 @@ def generation_page(parent,settings):
"width": 120,
"height": 50
}
}
}
diag = Tk()
diag.title("Door Shuffle " + __version__)
diag.title("Door Shuffle " + DRVersion)
diag.geometry(str(dims["window"]["width"]) + 'x' + str(dims["window"]["height"]))
text = Text(diag, width=dims["textarea.characters"]["width"], height=dims["textarea.characters"]["height"])
text.pack()

5
uv.lock generated
View File

@@ -11,6 +11,7 @@ resolution-markers = [
name = "aenum"
version = "3.1.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/09/7a/61ed58e8be9e30c3fe518899cc78c284896d246d51381bab59b5db11e1f3/aenum-3.1.16.tar.gz", hash = "sha256:bfaf9589bdb418ee3a986d85750c7318d9d2839c1b1a1d6fe8fc53ec201cf140", size = 137693, upload-time = "2026-01-12T22:34:38.819Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/52/6ad8f63ec8da1bf40f96996d25d5b650fdd38f5975f8c813732c47388f18/aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf", size = 165627, upload-time = "2025-04-25T03:17:58.89Z" },
]
@@ -41,8 +42,8 @@ wheels = [
]
[[package]]
name = "alttpoverworldrandomizer"
version = "0.1.0"
name = "alttpr-python"
version = "1.0.0"
source = { virtual = "." }
dependencies = [
{ name = "aenum" },