53 Commits
Author SHA1 Message Date
karafruit a019fa6478 romname starts with GK 2026-01-25 13:16:35 -06:00
karafruit a51be12a4a More romname fixes 2026-01-25 10:34:37 -06:00
karafruit 9e5db19c73 Change order of parts in rom name for compatibility with webmulti 2026-01-25 10:11:10 -06:00
karafruit 640a5bc3a7 Log exception trace on failed generation attempts 2026-01-24 22:16:47 -06:00
karafruit c946981c38 isort 2026-01-24 22:09:56 -06:00
karafruit 9733da9f44 Always include team number in rom name 2026-01-24 21:59:11 -06:00
karafruit a3f67a39ab Version/romnaming refactor 2026-01-24 20:51:48 -06:00
karafruit 1d4c5f1884 .gitattributes 2026-01-24 13:56:09 -06:00
karafruit a8dc25e59d Remove unnecessary references to data/base2current.json 2026-01-24 13:47:53 -06:00
karafruit 0f7253c94e Reduce tier of triforce pieces when goal is high 2026-01-23 21:41:45 -06:00
karafruit ddda8aeddf Guarantee a bee for sale 2026-01-23 18:56:20 -06:00
karafruit ae1744f0dd Update baserom 2026-01-21 19:47:43 -06:00
karafruit f2d07c98b7 Update baserom - kak music bug 2026-01-21 18:26:04 -06:00
karafruit 67307a872c Bosshunt mode 2026-01-21 08:20:59 -06:00
karafruit 3255c75828 Update baserom 2026-01-19 13:37:08 -06:00
karafruit 9db2a3b64b Update baserom 2026-01-18 16:57:23 -06:00
karafruit d424505677 Add attempt number to failure messages 2026-01-18 00:39:53 -06:00
karafruit 2a7141d24e Update baserom 2026-01-17 13:57:09 -06:00
karafruit 80a64b884e Update baserom 2026-01-17 13:31:10 -06:00
karafruit 5e0deadf55 showmap option 2026-01-17 12:54:17 -06:00
karafruit dd98e55d36 Move dungeon balancing message to debug level 2026-01-16 17:55:45 -06:00
karafruit 6c8a8e18e0 Add --tries arg 2026-01-16 16:43:12 -06:00
karafruit e058fd1961 Update baserom 2026-01-16 14:38:16 -06:00
karafruit bfd791fc87 Update baserom 2026-01-16 12:28:40 -06:00
karafruit b10c700836 Tweak loot/map values slightly 2026-01-15 19:03:51 -06:00
karafruit f5d5860af0 Update baserom 2026-01-15 13:52:41 -06:00
karafruit da0cef6dfe Update baserom 2026-01-15 13:47:20 -06:00
karafruit 36eb171d24 Merge branch 'GwaaKiwi' into DungeonMap 2026-01-15 04:08:13 -06:00
karafruit 2ce245c430 Update baserom 2026-01-14 18:23:10 -06:00
karafruit e8bc0d81c4 Update baserom 2026-01-14 13:10:48 -06:00
karafruit 5f66483d98 Update baserom 2026-01-14 11:33:53 -06:00
karafruit 30e85c1256 Update baserom 2026-01-13 19:45:10 -06:00
karafruit 402c9a5f76 Update baserom 2026-01-13 18:58:30 -06:00
karafruit 34b5dfb4e8 Update baserom 2026-01-13 11:01:07 -06:00
karafruit 54e73c0b99 Update baserom 2026-01-13 09:34:46 -06:00
karafruit affa210802 Update baserom 2026-01-13 00:34:45 -06:00
karafruit ea62a5bcb7 Update baserom, write better doors map flags 2026-01-13 00:27:27 -06:00
karafruit bd5fd9ec56 Update baserom, fix freezor chest? 2026-01-10 14:53:58 -06:00
karafruit 8df39da2bd Add showloot setting 2026-01-08 08:18:27 -06:00
karafruit 9b98f59292 Update baserom 2026-01-07 20:57:12 -06:00
karafruit 870d981eab Update baserom 2026-01-06 22:40:41 -06:00
karafruit a895fe158b Update baserom (should fix bad floor ids messing things up in doors) 2026-01-06 10:43:30 -06:00
karafruit 409c6d9a4e Update baserom 2026-01-04 23:04:09 -06:00
karafruit 42b4374bd3 Initial pass at door shuffle dungeon maps 2026-01-04 14:24:29 -06:00
karafruit 74739347e7 Update baserom 2026-01-04 12:58:18 -06:00
karafruit c94d556c4b Baserom update 2026-01-04 12:26:50 -06:00
karafruit 2195adca64 Add HC and AT compass if pots or enemies are shuffled 2026-01-04 09:44:29 -06:00
karafruit e5ad81861a Update baserom 2026-01-04 09:19:05 -06:00
karafruit 5418dbfb88 Update baserom, set item loot levels for crystals, pendants, and bomb bag 2026-01-04 07:17:24 -06:00
karafruit 507f4170c8 Update Baserom and write loot bits 2026-01-03 23:19:51 -06:00
karafruit b038ee6cbe Update baserom 2026-01-03 17:33:34 -06:00
karafruit 2d91647794 Update baserom 2026-01-03 17:28:56 -06:00
karafruit 7d95405252 Update baserom 2026-01-03 17:17:11 -06:00
107 changed files with 2913 additions and 7460 deletions
-81
View File
@@ -1,81 +0,0 @@
# GitHub Pages Deployment Workflow
name: 📄 Deploy GitHub Pages
# Deploy on push to DoorDevUnstable branch
on:
push:
branches:
- DoorDevUnstable
# Allow manual trigger
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
# Build job
build:
name: 🔨 Build Site
runs-on: ubuntu-latest
steps:
- name: ✔️ Checkout
uses: actions/checkout@v4
- name: 💎 Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1'
bundler-cache: false
- name: 💿 Install dependencies
run: |
gem install bundler
bundle config set --local path 'vendor/bundle'
# Create Gemfile if it doesn't exist
if [ ! -f Gemfile ]; then
cat > Gemfile << 'EOF'
source 'https://rubygems.org'
gem 'github-pages', group: :jekyll_plugins
gem 'jekyll-feed'
gem 'jekyll-seo-tag'
gem 'jekyll-sitemap'
gem 'just-the-docs'
EOF
fi
bundle install
- name: 🔨 Build with Jekyll
run: bundle exec jekyll build --baseurl "/ALttPDoorRandomizer"
env:
JEKYLL_ENV: production
- name: 📤 Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./_site
# Deployment job
deploy:
name: 🚀 Deploy to GitHub Pages
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: 🚀 Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: ✅ Deployment complete
run: |
echo "🎉 GitHub Pages deployed successfully!"
echo "📄 Site URL: ${{ steps.deployment.outputs.page_url }}"
-1
View File
@@ -31,7 +31,6 @@ weights/
/QUsb2Snes/ /QUsb2Snes/
/output/ /output/
/enemizer/ /enemizer/
visualizations/
resources/user/* resources/user/*
!resources/user/.gitkeep !resources/user/.gitkeep
+1
View File
@@ -0,0 +1 @@
<randall.rupper@gmail.com> <randall.rupper@sirsidynix.com>
+55 -87
View File
@@ -26,12 +26,11 @@ from Versions import DRVersion, GKVersion, ORVersion
class World(object): class World(object):
def __init__(self, players, owLayout, owParallel, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode):
self.players = players self.players = players
self.teams = 1 self.teams = 1
self.owLayout = owLayout.copy() self.owShuffle = owShuffle.copy()
self.owParallel = owParallel.copy()
self.owTerrain = {} self.owTerrain = {}
self.owKeepSimilar = {} self.owKeepSimilar = {}
self.owMixed = owMixed.copy() self.owMixed = owMixed.copy()
@@ -39,7 +38,6 @@ class World(object):
self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none' self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none'
self.owWhirlpoolShuffle = {} self.owWhirlpoolShuffle = {}
self.owFluteShuffle = {} self.owFluteShuffle = {}
self.owFog = {}
self.shuffle = shuffle.copy() self.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle.copy() self.doorShuffle = doorShuffle.copy()
self.intensity = {} self.intensity = {}
@@ -85,7 +83,6 @@ class World(object):
self.bosses_ganon = {} self.bosses_ganon = {}
self.bosshunt_include_agas = {} self.bosshunt_include_agas = {}
self.ganon_item = {} self.ganon_item = {}
self.require_ganon_item = {}
self.ganon_item_orig = {} self.ganon_item_orig = {}
self.custom = custom self.custom = custom
self.customitemarray = customitemarray self.customitemarray = customitemarray
@@ -102,9 +99,6 @@ class World(object):
self.owswaps = {} self.owswaps = {}
self.owcrossededges = {} self.owcrossededges = {}
self.owwhirlpools = {} self.owwhirlpools = {}
self.owgrid = {}
self.owlayoutmap_lw = {}
self.owlayoutmap_dw = {}
self.owflutespots = {} self.owflutespots = {}
self.owsectors = {} self.owsectors = {}
self.allow_flip_sanc = {} self.allow_flip_sanc = {}
@@ -137,7 +131,6 @@ class World(object):
set_player_attr('owswaps', [[],[],[]]) set_player_attr('owswaps', [[],[],[]])
set_player_attr('owcrossededges', []) set_player_attr('owcrossededges', [])
set_player_attr('owwhirlpools', []) set_player_attr('owwhirlpools', [])
set_player_attr('owgrid', None)
set_player_attr('owsectors', None) set_player_attr('owsectors', None)
set_player_attr('allow_flip_sanc', False) set_player_attr('allow_flip_sanc', False)
set_player_attr('remote_items', False) set_player_attr('remote_items', False)
@@ -165,7 +158,6 @@ class World(object):
set_player_attr('bigkeyshuffle', 'none') set_player_attr('bigkeyshuffle', 'none')
set_player_attr('prizeshuffle', 'none') set_player_attr('prizeshuffle', 'none')
set_player_attr('showloot', 'never') set_player_attr('showloot', 'never')
set_player_attr('loothud', 'never')
set_player_attr('showmap', 'map') set_player_attr('showmap', 'map')
set_player_attr('restrict_boss_items', 'none') set_player_attr('restrict_boss_items', 'none')
set_player_attr('bombbag', False) set_player_attr('bombbag', False)
@@ -187,7 +179,6 @@ class World(object):
set_player_attr('bosses_ganon', 8) set_player_attr('bosses_ganon', 8)
set_player_attr('bosshunt_include_agas', False) set_player_attr('bosshunt_include_agas', False)
set_player_attr('ganon_item', 'silver') set_player_attr('ganon_item', 'silver')
set_player_attr('require_ganon_item', False)
set_player_attr('crystals_ganon_orig', {}) set_player_attr('crystals_ganon_orig', {})
set_player_attr('crystals_gt_orig', {}) set_player_attr('crystals_gt_orig', {})
set_player_attr('ganon_item_orig', 'silver') set_player_attr('ganon_item_orig', 'silver')
@@ -205,7 +196,6 @@ class World(object):
set_player_attr('damage_challenge', 'normal') set_player_attr('damage_challenge', 'normal')
set_player_attr('shuffle_damage_table', 'vanilla') set_player_attr('shuffle_damage_table', 'vanilla')
set_player_attr('crystal_book', False) set_player_attr('crystal_book', False)
set_player_attr('extra_keys', 0)
set_player_attr('collection_rate', False) set_player_attr('collection_rate', False)
set_player_attr('colorizepots', True) set_player_attr('colorizepots', True)
set_player_attr('pot_pool', {}) set_player_attr('pot_pool', {})
@@ -357,16 +347,6 @@ class World(object):
def is_tile_lw_like(self, owid, player): def is_tile_lw_like(self, owid, player):
return (owid >= 0x40 and owid < 0x80) == self.is_tile_swapped(owid, player) return (owid >= 0x40 and owid < 0x80) == self.is_tile_swapped(owid, player)
def get_portal_unsafe(self, portal_name, player):
if (portal_name, player) in self._portal_cache:
return self._portal_cache[(portal_name, player)]
else:
for portal in self.dungeon_portals[player]:
if portal.name == portal_name and portal.player == player:
self._portal_cache[(portal_name, player)] = portal
return portal
return None
def is_atgt_swapped(self, player): def is_atgt_swapped(self, player):
return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player) return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player)
@@ -1409,7 +1389,7 @@ class CollectionState(object):
def can_hit_stunned_ganon(self, player): def can_hit_stunned_ganon(self, player):
ganon_item = self.world.ganon_item[player] ganon_item = self.world.ganon_item[player]
if ganon_item == "silver": if ganon_item == "silver":
return self.has("Silver Arrows", player) and self.can_shoot_arrows(player) return self.has("Silver Arroys", player) and self.can_shoot_arrows(player)
elif ganon_item == "boomerang": elif ganon_item == "boomerang":
return self.has("Blue Boomerang", player) or self.has("Red Boomerang", player) return self.has("Blue Boomerang", player) or self.has("Red Boomerang", player)
elif ganon_item == "hookshot": elif ganon_item == "hookshot":
@@ -1521,10 +1501,7 @@ class CollectionState(object):
if not item: if not item:
return return
changed = False changed = False
if item.name == "Sword and Shield": if item.name.startswith('Progressive '):
self.prog_items["Fighter Sword", item.player] += 1
self.prog_items["Blue Shield", item.player] += 1
elif item.name.startswith('Progressive '):
if 'Sword' in item.name: if 'Sword' in item.name:
if self.has('Golden Sword', item.player): if self.has('Golden Sword', item.player):
pass pass
@@ -1770,12 +1747,12 @@ class Entrance(object):
def can_reach(self, state): def can_reach(self, state):
# Destination Pickup OW Only No Ledges Can S&Q Allow Mirror # Destination Pickup OW Only No Ledges Can S&Q Allow Mirror
multi_step_locations = { 'Pyramid Crack': ('Pick Up Big Bomb', True, True, False, True), multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True),
'Missing Smith': ('Get Frog', True, False, True, True), 'Missing Smith': ('Frog', True, False, True, True),
'Middle Aged Man': ('Pick Up Purple Chest', True, False, True, True), 'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True),
'Dark Palace Button': ('Pick Up Kiki', True, False, False, False), 'Dark Palace Button':('Kiki', True, False, False, False),
'Old Man Drop Off': ('Escort Old Man', True, False, False, False), 'Old Man Drop Off': ('Lost Old Man', True, False, False, False),
'Revealing Light': ('Maiden Rescued', False, False, False, False) 'Revealing Light': ('Suspicious Maiden', False, False, False, False)
} }
if self.name in multi_step_locations: if self.name in multi_step_locations:
@@ -1784,10 +1761,7 @@ class Entrance(object):
multi_step_loc = multi_step_locations[self.name] multi_step_loc = multi_step_locations[self.name]
if world.shuffle_followers[self.player]: if world.shuffle_followers[self.player]:
multi_step_loc = (multi_step_loc[0], self.name == 'Pyramid Crack', multi_step_loc[2], True, True) multi_step_loc = (multi_step_loc[0], self.name == 'Pyramid Crack', multi_step_loc[2], True, True)
step_location = world.find_items(multi_step_loc[0], self.player) step_location = world.get_location(multi_step_loc[0], self.player)
if len(step_location) == 0:
return False
step_location = step_location[0]
if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_loc[1], multi_step_loc[2], multi_step_loc[3], multi_step_loc[4]) and self.access_rule(state): if step_location.can_reach(state) and self.can_reach_thru(state, step_location, multi_step_loc[1], multi_step_loc[2], multi_step_loc[3], multi_step_loc[4]) and self.access_rule(state):
if not self in state.path: if not self in state.path:
path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None)) path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None))
@@ -1985,7 +1959,6 @@ class Dungeon(object):
self.prize = None self.prize = None
self.big_key = big_key self.big_key = big_key
self.small_keys = small_keys self.small_keys = small_keys
self.extra_small_keys = 0
self.dungeon_items = dungeon_items self.dungeon_items = dungeon_items
self.bosses = dict() self.bosses = dict()
self.player = player self.player = player
@@ -2428,11 +2401,12 @@ class OWEdge(object):
def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
self.player = player self.player = player
self.name = name self.name = name
self.type = DoorType.Open
self.direction = direction self.direction = direction
self.terrain = terrain self.terrain = terrain
self.parallel = None
self.specialEntrance = False self.specialEntrance = False
self.specialExit = False self.specialExit = False
self.deadEnd = False
# rom properties # rom properties
self.owIndex = owIndex self.owIndex = owIndex
@@ -2463,6 +2437,7 @@ class OWEdge(object):
self.worldType = WorldType.Dark self.worldType = WorldType.Dark
# logical properties # logical properties
# self.connected = False # combine with Dest?
self.dest = None self.dest = None
self.dependents = [] self.dependents = []
self.dead = False self.dead = False
@@ -2479,6 +2454,9 @@ class OWEdge(object):
def getTarget(self): def getTarget(self):
return self.dest.specialID if self.dest.specialExit else self.dest.edge_id return self.dest.specialID if self.dest.specialExit else self.dest.edge_id
def dead_end(self):
self.deadEnd = True
def coordInfo(self, midpoint, vram_loc): def coordInfo(self, midpoint, vram_loc):
self.midpoint = midpoint self.midpoint = midpoint
self.vramLoc = vram_loc self.vramLoc = vram_loc
@@ -2900,10 +2878,6 @@ class Item(object):
def compass(self): def compass(self):
return self.type == 'Compass' return self.type == 'Compass'
@property
def event(self):
return self.type == 'Event'
@property @property
def dungeon(self): def dungeon(self):
if not self.smallkey and not self.bigkey and not self.map and not self.compass: if not self.smallkey and not self.bigkey and not self.map and not self.compass:
@@ -3121,15 +3095,13 @@ class Spoiler(object):
'bow_mode': self.world.bow_mode, 'bow_mode': self.world.bow_mode,
'goal': self.world.goal, 'goal': self.world.goal,
'custom_goals': self.world.custom_goals, 'custom_goals': self.world.custom_goals,
'ow_layout': self.world.owLayout, 'ow_shuffle': self.world.owShuffle,
'ow_parallel': self.world.owParallel,
'ow_terrain': self.world.owTerrain, 'ow_terrain': self.world.owTerrain,
'ow_crossed': self.world.owCrossed, 'ow_crossed': self.world.owCrossed,
'ow_keepsimilar': self.world.owKeepSimilar, 'ow_keepsimilar': self.world.owKeepSimilar,
'ow_mixed': self.world.owMixed, 'ow_mixed': self.world.owMixed,
'ow_whirlpool': self.world.owWhirlpoolShuffle, 'ow_whirlpool': self.world.owWhirlpoolShuffle,
'ow_fluteshuffle': self.world.owFluteShuffle, 'ow_fluteshuffle': self.world.owFluteShuffle,
'ow_fog': self.world.owFog,
'bonk_drops': self.world.shuffle_bonk_drops, 'bonk_drops': self.world.shuffle_bonk_drops,
'shuffle_followers': self.world.shuffle_followers, 'shuffle_followers': self.world.shuffle_followers,
'shuffle': self.world.shuffle, 'shuffle': self.world.shuffle,
@@ -3156,7 +3128,6 @@ class Spoiler(object):
'ganon_bosses': self.world.bosses_ganon, 'ganon_bosses': self.world.bosses_ganon,
'bosshunt_include_agas': self.world.bosshunt_include_agas, 'bosshunt_include_agas': self.world.bosshunt_include_agas,
'ganon_item': self.world.ganon_item, 'ganon_item': self.world.ganon_item,
'require_ganon_item': self.world.require_ganon_item,
'open_pyramid': self.world.open_pyramid, 'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility, 'accessibility': self.world.accessibility,
'restricted_boss_items': self.world.restrict_boss_items, 'restricted_boss_items': self.world.restrict_boss_items,
@@ -3167,7 +3138,6 @@ class Spoiler(object):
'bigkeyshuffle': self.world.bigkeyshuffle, 'bigkeyshuffle': self.world.bigkeyshuffle,
'prizeshuffle': self.world.prizeshuffle, 'prizeshuffle': self.world.prizeshuffle,
'showloot': self.world.showloot, 'showloot': self.world.showloot,
'loothud': self.world.loothud,
'showmap': self.world.showmap, 'showmap': self.world.showmap,
'boss_shuffle': self.world.boss_shuffle, 'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle, 'enemy_shuffle': self.world.enemy_shuffle,
@@ -3188,7 +3158,6 @@ class Spoiler(object):
'damage_challenge': self.world.damage_challenge, 'damage_challenge': self.world.damage_challenge,
'shuffle_damage_table': self.world.shuffle_damage_table, 'shuffle_damage_table': self.world.shuffle_damage_table,
'crystal_book': self.world.crystal_book, 'crystal_book': self.world.crystal_book,
'extra_keys': self.world.extra_keys,
'triforcegoal': self.world.treasure_hunt_count, 'triforcegoal': self.world.treasure_hunt_count,
'triforcepool': self.world.treasure_hunt_total, 'triforcepool': self.world.treasure_hunt_total,
'race': self.world.settings.world_rep['meta']['race'], 'race': self.world.settings.world_rep['meta']['race'],
@@ -3196,8 +3165,6 @@ class Spoiler(object):
'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)},
'seed': self.world.seed 'seed': self.world.seed
} }
if self.hashes:
self.metadata['hash'] = {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)} # TODO: make this work for multiple teams
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
@@ -3394,8 +3361,7 @@ class Spoiler(object):
if custom['murahgoal'] and 'requirements' in custom['murahgoal']: if custom['murahgoal'] and 'requirements' in custom['murahgoal']:
outfile.write('Murahdahla Requirement:'.ljust(line_width) + 'custom\n') outfile.write('Murahdahla Requirement:'.ljust(line_width) + 'custom\n')
outfile.write(' %s\n' % custom['murahgoal']['goaltext']) outfile.write(' %s\n' % custom['murahgoal']['goaltext'])
outfile.write('Item to Hurt Stunned Ganon:'.ljust(line_width) + '%s\n' % str(self.world.ganon_item_orig[player])) outfile.write('Item Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.ganon_item_orig[player]))
outfile.write('Item Required for Ganon:'.ljust(line_width) + '%s\n' % yn(self.world.require_ganon_item[player]))
outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player])
outfile.write('\n') outfile.write('\n')
outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player])
@@ -3415,18 +3381,15 @@ class Spoiler(object):
outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player])
outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player])
outfile.write('\n') outfile.write('\n')
outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_layout'][player]) outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player])
if self.metadata['ow_layout'][player] != 'vanilla': if self.metadata['ow_shuffle'][player] != 'vanilla':
outfile.write('Parallel OW:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_parallel'][player]))
outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player]))
outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player])
if self.metadata['ow_layout'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none':
outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player]))
outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player]))
outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player]))
outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player])
if self.metadata['ow_layout'][player] == 'grid' or self.metadata['ow_mixed'][player]:
outfile.write('Overworld Fog:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_fog'][player]))
outfile.write('\n') outfile.write('\n')
outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != 'vanilla': if self.metadata['shuffle'][player] != 'vanilla':
@@ -3445,7 +3408,6 @@ class Spoiler(object):
outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][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('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 Value of Checks:'.ljust(line_width) + '%s\n' % self.metadata['showloot'][player])
outfile.write('Show Value of Checks on HUD:'.ljust(line_width) + '%s\n' % self.metadata['loothud'][player])
outfile.write('Show Map:'.ljust(line_width) + '%s\n' % self.metadata['showmap'][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('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player])
outfile.write('\n') outfile.write('\n')
@@ -3472,7 +3434,6 @@ class Spoiler(object):
outfile.write('Damage Challenge:'.ljust(line_width) + '%s\n' % self.metadata['damage_challenge'][player]) outfile.write('Damage Challenge:'.ljust(line_width) + '%s\n' % self.metadata['damage_challenge'][player])
outfile.write('Damage Table Randomization:'.ljust(line_width) + '%s\n' % self.metadata['shuffle_damage_table'][player]) outfile.write('Damage Table Randomization:'.ljust(line_width) + '%s\n' % self.metadata['shuffle_damage_table'][player])
outfile.write('Crystal Book:'.ljust(line_width) + '%s\n' % yn(self.metadata['crystal_book'][player])) outfile.write('Crystal Book:'.ljust(line_width) + '%s\n' % yn(self.metadata['crystal_book'][player]))
outfile.write('Extra Keys:'.ljust(line_width) + '%d%%\n' % self.metadata['extra_keys'][player])
outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player]))
outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race']))
@@ -3545,31 +3506,40 @@ class Spoiler(object):
outfile.write(f'{fairy}: {bottle}\n') outfile.write(f'{fairy}: {bottle}\n')
if self.maps: if self.maps:
def write_map(type, title):
for player in range(1, self.world.players + 1):
if (type, player) in self.maps:
outfile.write('\n\n' + title + '\n\n')
break
for player in range(1, self.world.players + 1):
if (type, player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[(type, player)]['text'])
if 'all' in self.settings or 'flute' in self.settings: if 'all' in self.settings or 'flute' in self.settings:
# flute shuffle # flute shuffle
write_map('flute', 'Flute Spots:') for player in range(1, self.world.players + 1):
if ('flute', player) in self.maps:
outfile.write('\n\nFlute Spots:\n\n')
break
for player in range(1, self.world.players + 1):
if ('flute', player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[('flute', player)]['text'])
if 'all' in self.settings or 'overworld' in self.settings: if 'all' in self.settings or 'overworld' in self.settings:
# overworld tile flips # overworld tile flips
write_map('swaps', 'OW Tile Flips:') for player in range(1, self.world.players + 1):
if ('swaps', player) in self.maps:
outfile.write('\n\nOW Tile Flips:\n\n')
break
for player in range(1, self.world.players + 1):
if ('swaps', player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[('swaps', player)]['text'])
# crossed groups # crossed groups
write_map('groups', 'OW Crossed Groups:') for player in range(1, self.world.players + 1):
if ('groups', player) in self.maps:
# grid layout outfile.write('\n\nOW Crossed Groups:\n\n')
write_map('layout_grid_lw', 'Light World Layout:') break
write_map('layout_grid_dw', 'Dark World Layout:') for player in range(1, self.world.players + 1):
if ('groups', player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[('groups', player)]['text'])
if self.overworlds and ('all' in self.settings or 'overworld' in self.settings): if self.overworlds and ('all' in self.settings or 'overworld' in self.settings):
outfile.write('\n\nOverworld Edges:\n\n') outfile.write('\n\nOverworld Edges:\n\n')
@@ -3834,11 +3804,11 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
# byte 10: settings_version # byte 10: settings_version
# byte 11: POOT WCCC (parallel, OWR layout, free terrain, whirlpools, OWR crossed) # byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed)
orlayout_mode = {"vanilla": 0, "grid": 1, "wild": 2} or_mode = {"vanilla": 0, "parallel": 1, "full": 2}
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4} orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4}
# byte 12: KMBQ FFO? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots, fog) # byte 12: KMBQ FF?? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots)
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}
# byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle) # byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle)
@@ -3901,12 +3871,12 @@ class Settings(object):
settings_version, settings_version,
(0x80 if w.owParallel[p] else 0) | (orlayout_mode[w.owLayout[p]] << 5) | (0x10 if w.owTerrain[p] else 0) (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0)
| (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]], | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]],
(0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) (0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0)
| (0x20 if w.shuffle_bonk_drops[p] else 0) | (0x10 if w.shuffle_followers[p] else 0) | (0x20 if w.shuffle_bonk_drops[p] else 0) | (0x10 if w.shuffle_followers[p] else 0)
| (flutespot_mode[w.owFluteShuffle[p]] << 4) | (0x02 if w.owFog[p] else 0), | (flutespot_mode[w.owFluteShuffle[p]] << 4),
(flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4
| take_any_mode[w.take_any[p]] << 2 | prizeshuffle_mode[w.prizeshuffle[p]]), | take_any_mode[w.take_any[p]] << 2 | prizeshuffle_mode[w.prizeshuffle[p]]),
@@ -3983,8 +3953,7 @@ class Settings(object):
args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3] args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)] args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
args.ow_parallel[p] = True if settings[11] & 0x80 else False args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5]
args.ow_layout[p] = r(orlayout_mode)[(settings[11] & 0x60) >> 5]
args.ow_terrain[p] = True if settings[11] & 0x10 else False args.ow_terrain[p] = True if settings[11] & 0x10 else False
args.ow_whirlpool[p] = True if settings[11] & 0x08 else False args.ow_whirlpool[p] = True if settings[11] & 0x08 else False
args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)] args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)]
@@ -3994,7 +3963,6 @@ class Settings(object):
args.bonk_drops[p] = True if settings[12] & 0x20 else False args.bonk_drops[p] = True if settings[12] & 0x20 else False
args.shuffle_followers[p] = True if settings[12] & 0x10 else False args.shuffle_followers[p] = True if settings[12] & 0x10 else False
args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2] args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2]
args.ow_fog[p] = True if settings[12] & 0x02 else False
if len(settings) > 13: if len(settings) > 13:
args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7] args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7]
-53
View File
@@ -1,58 +1,5 @@
# Changelog # Changelog
# 0.7.0.3
- Further updates and new yamls for Grid OW Shuffle
- Further customizer options for Grid OW Shuffle like defining screens that should stay together
- Fix buffer sword slash when dashing into water
- Fixed bug with MSU-1 GT2 track not falling back to GT track
- Fix for Kiki unfollowing after certain entrance transition conditions
- Fixed bug/oversight with vanilla_fill and prize_shuffle
- Changed big bomb shop vendor text to be follower agnostic
- Updated crosskeys winners (thanks clearmouse)
- Fixed bug with potential locale determination (thanks Esme)
- \~Merged in DR v1.5.6~
# 0.7.0.2
- Fixed money error for bombbag/take-anys
- Fixed palette issue for map check/flute menu
- Fixed issue with dungeon icons showing in opposite world in Inverted
- Fixed Links position in map checks when in Special OW areas
- Fixed buffered sword issue in OW Shuffle water transitions
- Added PlasmaKappa tribute TF room text
# 0.7.0.1
- Fixed buggy sprites in post-Aga Zora's Domain
- Fixed L/R map switch when in special OW screens
- Fixes issue not able to screen transition if bumped by enemy in water
# 0.7.0.0
- New OW Layout Shuffle Mode: Grid
- Implemented Fog of War for Tile Flip
# 0.6.1.11
- Fixed bonk drops duplicate counting and potentially overwriting arbitrary values
- Fixed boss icons on dungeon map check
- Fixed dungeon counters to autotrack correctly
- Key and chest counts in menu now display consistently (must have dungeon item or have visited dungeon to see the HUD)
- Money balancing will fail in less scenarios
- Fixed issue with Sanc pots not collecting
- Enemizer now allows more enemies on water
- Fix infinite pit fall issue with Old Man follower location
- Fix bad overworld tilemap drawing on HC and Pyramid screens in OW Layout Shuffle
## 0.6.1.10
- Emergency fix for bonk functionality
## 0.6.1.9
- Fixed follower shuffle placement errors
- Fixed pseudoboots ability to open Kings Tomb
- Implemented better accurate coordinates on map check locations
- Fixed janky icons on zoomed-in map check screen
## 0.6.1.8
- Fixed follower placement and logic
- Fixed error with HC Courtyard Tree Pull
## 0.6.1.7 ## 0.6.1.7
- \~Merged in DR v1.5.2~ - \~Merged in DR v1.5.2~
- Reverted key count update - Reverted key count update
-257
View File
@@ -1,257 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**ALttPDoorRandomizer** is a sophisticated dungeon and door randomizer for *The Legend of Zelda: A Link to the Past* (SNES). It extends the standard ALTTP Entrance Randomizer with advanced dungeon shuffling capabilities that can rearrange dungeon interiors at the door and sector level.
- **Repository**: https://github.com/aerinon/ALttPDoorRandomizer
- **Python Version**: 3.10+
- **Current Branch**: DoorDevUnstable (dev branch: NewGeneration)
- **Discord**: #door-rando and #bug-reports channels at ALTTP Randomizer discord
## Development Commands
### Running the Randomizer
```bash
# CLI interface
python DungeonRandomizer.py
# GUI interface
python Gui.py
```
### Dependencies
```bash
# Install platform-specific dependencies
python resources/ci/common/local_install.py
# For multiworld server/client
# Run the same command to install multiworld dependencies
```
### Testing
```bash
# Run test suite
python TestSuite.py
# Run test suite with specific door shuffle mode
python TestSuite.py --dr basic --count 10 --tense 2
# Individual test files (unittest-based)
python -m pytest test/
python -m pytest test/dungeons/TestDarkPalace.py
```
### ROM Generation
```bash
# Basic door shuffle seed
python DungeonRandomizer.py --doorShuffle crossed --intensity 2
# Suppress ROM output (for testing)
python DungeonRandomizer.py --suppress_rom --spoiler none
# Create BPS patch
python DungeonRandomizer.py --bps
```
## Architecture Overview
### Generation Pipeline
The randomizer follows this high-level flow (orchestrated in `Main.py`):
1. **World Setup** → Initialize World object, parse settings, set seed
2. **Region Creation** → Build game regions, locations, entrances (`Regions.py`)
3. **Dungeon Structure** → Create dungeon objects and rooms (`Dungeons.py`, `RoomData.py`)
4. **Door/Entrance Shuffling** → Shuffle dungeon interiors and/or overworld entrances
5. **Logic Rules** → Set access requirements for all locations (`Rules.py`)
6**Item Placement** → Generate item pool and place items with logic constraints (`Fill.py`)
7. **ROM Patching** → Apply changes to base ROM (`Rom.py`)
8. **Output** → Generate ROM file, spoiler log, and/or multiworld data
### Core Data Structures (BaseClasses.py)
- **World**: Container for all game state, regions, dungeons, items, and settings
- **Region**: Geographic area with locations and exits
- **Location**: Item placement spot with access rules
- **Item**: Game items with advancement/priority flags
- **Entrance**: Connections between regions with conditional access
- **CollectionState**: Tracks player's collected items for logic evaluation
### Dungeon Generation System
The dungeon shuffling system uses a three-stage algorithm:
#### 1. DungeonGenLocalSearch (source/dungeon/DungeonGenLocalSearch.py)
**Purpose**: Assigns dungeon sectors to dungeons using constraint satisfaction
**Balancing Constraints**:
- **Polarity**: North/South and East/West door balance
- **Crystal Balance**: Blue/orange barriers must have matching switches
- **Portal Balance**: Entrance distribution
- **Location Balance**: Ensures dungeons have accessible non-big-key locations
- **Branching**: Dead ends vs branches ratio (insufficient branches = unreachable areas)
- **Transitivity**: Validates that doors can actually connect
**Algorithm**: Local search with greedy constraint satisfaction, prioritizing crystal switches → portals → locations → branching → polarity → transitivity
#### 2. DungeonGenTransitivity (source/dungeon/DungeonGenTransitivity.py)
**Purpose**: Validates that door connections are possible for a given sector assignment
**Key Features**:
- Verifies all doors can be validly connected (e.g., North↔South, East↔West, Stairs↔Stairs)
- Propagates crystal switch states through connections
- Detects forced connections and impossible layouts
- Uses depth-first search with constraint propagation
- Handles special mechanics (portals, crystal barriers, special rooms)
#### 3. DungeonStitcherV2 (source/dungeon/DungeonStitcherV2.py)
**Purpose**: Realizes actual door connections from validated sector assignments
**Key Features**:
- Generates random valid door pairings
- Assigns portal connections (dungeon entrances)
- Simulates player traversal to verify connectivity
- Manages crystal barrier state propagation
### Key Logic Components
**Rules.py** (~7000 lines): Sets logical requirements for accessing locations
- Supports multiple logic modes: noglitches, owglitches, hybridglitches, nologic
- Handles multiple key logic algorithms: partial, strict, dangerous, experimental
- Manages dungeon-specific access rules and boss requirements
**Fill.py** (~2000 lines): Item placement algorithms
- **Balanced**: Random distribution
- **Vanilla Fill**: Attempts vanilla item locations when possible
- **Major Location Restriction**: Major items → major locations
- **Dungeon Restriction**: Major items → dungeons
- **District Restriction**: Items distributed by geographic region
**KeyPlacement.py** / **NewKeyLogic.py**: Advanced key door logic
- Handles small key placement with multiple satisfiability algorithms
- Prevents self-locking situations
- Manages big key placement rules
- Validates minimum key requirements for dungeon completion
### Source Module Organization
```
source/
├── dungeon/ # Dungeon generation algorithms and key logic
├── overworld/ # Entrance shuffling (EntranceShuffle2.py)
├── item/ # Item placement utilities (FillUtil.py, District.py)
├── enemizer/ # Enemy randomization system
├── gui/ # GUI components (tkinter-based)
├── rom/ # ROM patching utilities
├── classes/ # Utility classes (BabelFish, CustomSettings)
├── meta/ # Metadata and build tools
└── limited/ # Limited run coordination
```
### Important Files by Size/Complexity
- **DoorShuffle.py** (~8500 lines): Door shuffling orchestration
- **Rules.py** (~7000 lines): Location access logic
- **EnemyList.py** (~6000 lines): Enemy definitions
- **Doors.py** (~5500 lines): Door object definitions
- **Rom.py** (~5500 lines): ROM patching
- **Regions.py** (~5000 lines): World region creation
- **DungeonGenerator.py** (~5000 lines): Legacy dungeon generation
- **EntranceShuffle2.py** (~5000 lines): Entrance shuffling logic
- **BaseClasses.py** (~4000 lines): Core data structures
- **KeyDoorShuffle.py** (~3500 lines): Key door logic
- **PotShuffle.py** (~3500 lines): Pot randomization
## Key Concepts
### Door Shuffle Modes
- **Vanilla**: No shuffling
- **Basic**: Shuffle within each dungeon
- **Partitioned**: Shuffle in pools (Light World, Early Dark World, Late Dark World)
- **Crossed**: Full shuffle between all dungeons
### Intensity Levels
- **Level 1**: Normal doors and spiral staircases
- **Level 2**: + open edges and straight staircases
- **Level 3**: + dungeon lobbies
### Key Shuffle Modes
- **In Dungeon** (keyshuffle=none): Keys restricted to their dungeon
- **Randomized** (keyshuffle=wild): Keys can be anywhere
- **Universal** (keyshuffle=universal): Keys work in any dungeon
### Fill Algorithms (--algorithm)
See Fill.py for implementation details. The algorithm affect where item may be placed.
### Logic Modes
- **noglitches**: Standard logic
- **owglitches**: Overworld glitches (OOB, clips, superbunny)
- **hybridglitches**: + major underworld glitches (not compatible with door shuffle)
- **nologic**: No logical constraints
## Testing Strategy
Tests verify location accessibility with various item combinations:
- **test/dungeons/**: Per-dungeon location access tests
- **test/vanilla/**: Vanilla world logic tests
- **test/owg/**: Overworld glitch logic tests
- **test/inverted/**: Inverted mode tests
- **TestBase.py**: Base test class with utility methods
Use `TestSuite.py` for batch generation testing across modes (Open/Standard/Inverted) and settings.
## Important Development Notes
### Starting Items & Special Items
- **Mirror Scroll**: Dungeon-only mirror (starting item in door shuffle)
- **Bomb Bag**: Optional item that gates bomb usage
- **Pseudo Boots**: Allows dashing but gates certain sequence breaks
### Branch Structure
- **DoorDev**: Main development branch (use for PRs)
- **DoorDevUnstable**: Current working branch
- **Dev/Master**: Do NOT use for PRs
### Git Workflow
Recent commits show work on:
- Limited run coordination
- Custom rooms support
- Key logic improvements (transitivity, portal checks, placement rules)
- Isolated region detection fixes
- Big key placement satisfiability
### Documentation Resources
- **Algorithm.md**: Detailed dungeon generation algorithm documentation
- **docs/ai_context/**: Comprehensive architectural documentation by topic
- **docs/Customizer.md**: Custom seed configuration guide
- **docs/DR_hint_reference.md**: Hint system documentation
- **docs/BUILDING.md**: Build and installation instructions
- **README.md**: User-facing feature documentation
## Common Gotchas
1. **Transitivity Checks**: Most expensive constraint check - always run last
2. **Self-Locking Keys**: Key logic must prevent situations where players cannot access the rest of the dungeon
3. **Crystal Barriers**: Blue/orange barriers require matching switches in dungeon
4. **Portal Distribution**: Each dungeon needs at least one entrance
5. **Branching Balance**: Too many dead ends without branches = unreachable areas
6**Polarity Balance**: N/S and E/W door counts must allow valid connections
## CLI Argument Reference
Key arguments from `resources/app/cli/args.json`:
- `--doorShuffle [vanilla|basic|partitioned|crossed]`
- `--intensity [1|2|3]`
- `--keyshuffle [none|wild|universal]`
- `--key_logic [partial|strict|dangerous|experimental]`
- `--algorithm [balanced|vanilla_fill|major_only|dungeon_only|district]`
- `--logic [noglitches|owglitches|hybridglitches|nologic]`
- `--mode [open|standard|inverted]`
- `--shuffle`: Entrance randomizer options (vanilla, simple, restricted, full, crossed, insanity, etc.)
- `--pottery`: Pot shuffling modes
- `--shopsanity`: Enable shop item randomization
- `--enemizer`: Enemy randomization options
+20 -69
View File
@@ -89,7 +89,6 @@ def parse_cli(argv, no_defaults=False):
parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli", "help", "count")), type=int) parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli", "help", "count")), type=int)
parser.add_argument('--tries', default=defval(int(settings["tries"]) if settings["tries"] != "" and settings["tries"] is not None else 1), help="\n".join(fish.translate("cli", "help", "tries")), type=int) parser.add_argument('--tries', default=defval(int(settings["tries"]) if settings["tries"] != "" and settings["tries"] is not None else 1), help="\n".join(fish.translate("cli", "help", "tries")), type=int)
parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS) parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS)
parser.add_argument('--skip_money_balance', action="store_true", help=argparse.SUPPRESS)
# included for backwards compatibility # included for backwards compatibility
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
@@ -107,14 +106,7 @@ def parse_cli(argv, no_defaults=False):
ret = parser.parse_args(argv) ret = parser.parse_args(argv)
if ret.keysanity: if ret.keysanity:
if ret.mapshuffle == "none": ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = ['wild'] * 4
ret.mapshuffle = "wild"
if ret.compassshuffle == "none":
ret.compassshuffle = "wild"
if ret.keyshuffle == "none":
ret.keyshuffle = "wild"
if ret.bigkeyshuffle == "none":
ret.bigkeyshuffle = "wild"
if ret.keydropshuffle: if ret.keydropshuffle:
ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle
@@ -128,66 +120,33 @@ def parse_cli(argv, no_defaults=False):
ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any
ret.keyshuffle = 'universal' ret.keyshuffle = 'universal'
if ret.ow_unparallel:
ret.ow_parallel = False
if ret.ow_shuffle == 'parallel':
ret.ow_layout = 'wild'
ret.ow_parallel = True
elif ret.ow_shuffle == 'full':
ret.ow_layout = 'wild'
ret.ow_parallel = False
if ret.ow_no_fog:
ret.ow_fog = False
if player_num: if player_num:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
for player in range(1, player_num + 1): for player in range(1, player_num + 1):
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True) playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
if playerargs.filename: if playerargs.filename:
playersettings = apply_settings_file({}, playerargs.filename) playersettings = apply_settings_file({}, playerargs.filename)
for k, v in playersettings.items(): for k, v in playersettings.items():
setattr(playerargs, k, v) setattr(playerargs, k, v)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle',
'item_functionality', 'ow_shuffle', 'ow_layout', 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
'ow_parallel', 'ow_terrain', 'ow_crossed', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers',
'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'bosshunt_include_agas', 'ganon_item', 'openpyramid',
'ow_fluteshuffle', 'ow_fog', 'flute_mode', 'bow_mode', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', 'startinventory',
'take_any', 'boots_hint', 'shuffle_followers', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
'shuffle', 'door_shuffle', 'intensity', 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern',
'bosshunt_include_agas', 'ganon_item', 'skullwoods', 'linked_drops',
'require_ganon_item', 'openpyramid', 'pseudoboots', 'mirrorscroll', 'dark_rooms', 'damage_challenge', 'shuffle_damage_table', 'crystal_book', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'bigkeyshuffle', 'prizeshuffle', 'showloot', 'loothud', 'ow_palettes', 'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'showmap', 'startinventory', 'usestartinventory', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
'bombbag', 'shuffleganon', 'overworld_map', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'shuffle_sfxinstruments',
'restrict_boss_items', 'triforce_max_difference', 'shuffle_songinstruments', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode',
'triforce_pool_min', 'triforce_pool_max', 'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness',
'triforce_goal_min', 'triforce_goal_max', 'money_balance']:
'triforce_min_difference', 'triforce_goal',
'triforce_pool', 'shufflelinks', 'shuffletavern',
'skullwoods', 'linked_drops', 'pseudoboots',
'mirrorscroll', 'dark_rooms', 'damage_challenge',
'shuffle_damage_table', 'crystal_book', 'extra_keys',
'retro', 'accessibility', 'hints', 'beemizer',
'experimental', 'dungeon_counters', 'shufflebosses',
'shuffleenemies', 'enemy_health', 'enemy_damage',
'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite',
'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu',
'heartcolor', 'heartbeep', 'remote_items', 'shopsanity',
'dropshuffle', 'pottery', 'keydropshuffle',
'mixed_travel', 'standardize_palettes', 'code',
'reduce_flashing', 'shuffle_sfx',
'shuffle_sfxinstruments', 'shuffle_songinstruments',
'msu_resume', 'collection_rate', 'colorizepots',
'decoupledoors', 'door_type_mode', 'bonk_drops',
'trap_door_mode', 'key_logic_algorithm',
'door_self_loops', 'any_enemy_logic', 'aga_randomness',
'money_balance', 'patches']:
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})
@@ -221,7 +180,6 @@ def parse_settings():
"bosses_ganon": "8", "bosses_ganon": "8",
"bosshunt_include_agas": False, "bosshunt_include_agas": False,
"ganon_item": "silver", "ganon_item": "silver",
"require_ganon_item": False,
"swords": "random", "swords": "random",
"flute_mode": "normal", "flute_mode": "normal",
"bow_mode": "progressive", "bow_mode": "progressive",
@@ -238,18 +196,13 @@ def parse_settings():
# Shuffle Ganon defaults to TRUE # Shuffle Ganon defaults to TRUE
"openpyramid": "auto", "openpyramid": "auto",
"shuffleganon": True, "shuffleganon": True,
"ow_shuffle": "vanilla", # for backwards compatibility "ow_shuffle": "vanilla",
"ow_layout": "vanilla",
"ow_parallel": True,
"ow_unparallel": False,
"ow_terrain": False, "ow_terrain": False,
"ow_crossed": "none", "ow_crossed": "none",
"ow_keepsimilar": False, "ow_keepsimilar": False,
"ow_mixed": False, "ow_mixed": False,
"ow_whirlpool": False, "ow_whirlpool": False,
"ow_fluteshuffle": "vanilla", "ow_fluteshuffle": "vanilla",
"ow_fog": True,
"ow_no_fog": False,
"shuffle_followers": False, "shuffle_followers": False,
"bonk_drops": False, "bonk_drops": False,
"shuffle": "vanilla", "shuffle": "vanilla",
@@ -265,7 +218,6 @@ def parse_settings():
"damage_challenge": "normal", "damage_challenge": "normal",
"shuffle_damage_table": "vanilla", "shuffle_damage_table": "vanilla",
"crystal_book": False, "crystal_book": False,
"extra_keys": 0,
"shuffleenemies": "none", "shuffleenemies": "none",
"shufflebosses": "none", "shufflebosses": "none",
@@ -285,7 +237,6 @@ def parse_settings():
"bigkeyshuffle": "none", "bigkeyshuffle": "none",
"prizeshuffle": "none", "prizeshuffle": "none",
"showloot": "never", "showloot": "never",
"loothud": "never",
"showmap": "map", "showmap": "map",
"keysanity": False, "keysanity": False,
"door_shuffle": "vanilla", "door_shuffle": "vanilla",
+4 -52
View File
@@ -3,7 +3,6 @@ import time
from collections import defaultdict, deque from collections import defaultdict, deque
from enum import Flag, unique from enum import Flag, unique
from itertools import chain from itertools import chain
from math import ceil
from typing import DefaultDict, Dict, List from typing import DefaultDict, Dict, List
import RaceRandom as random import RaceRandom as random
@@ -141,9 +140,6 @@ def link_doors_prep(world, player):
world.get_portal('Turtle Rock Eye Bridge', player).destination = True world.get_portal('Turtle Rock Eye Bridge', player).destination = True
else: else:
analyze_portals(world, player) analyze_portals(world, player)
if world.doorShuffle[player] == 'vanilla': # these are always not destinations
world.get_portal('Desert Back', player).destination = False
world.get_portal('Skull 3', player).destination = False
for portal in world.dungeon_portals[player]: for portal in world.dungeon_portals[player]:
connect_portal(portal, world, player) connect_portal(portal, world, player)
@@ -273,21 +269,11 @@ def vanilla_key_logic(world, player):
enabled_entrances = world.enabled_entrances[player] = {} enabled_entrances = world.enabled_entrances[player] = {}
builder_queue = deque(builders) builder_queue = deque(builders)
last_key, loops = None, 0 last_key, loops = None, 0
# --- Precompute all potential portals for each builder using dungeon_portals ---
from DungeonGenerator import dungeon_portals
all_potential_portals_map = {}
for builder in builders:
portal_names = list(dungeon_portals.get(builder.name, []))
all_potential_portals_map[builder.name] = set(world.get_portal(portal_name, player).door.entrance.parent_region.name for portal_name in portal_names)
while len(builder_queue) > 0: while len(builder_queue) > 0:
builder = builder_queue.popleft() builder = builder_queue.popleft()
origin_list = entrances_map[builder.name] origin_list = entrances_map[builder.name]
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
all_potential_origins = all_potential_portals_map[builder.name] if len(origin_list) <= 0:
enabled = entrances_map[builder.name]
if len(origin_list) <= 0 or should_delay_processing(enabled, all_potential_origins, potentials, connections, world, player):
if last_key == builder.name or loops > 1000: if last_key == builder.name or loops > 1000:
origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name
if len(origin_list) > 0 else 'no origin') if len(origin_list) > 0 else 'no origin')
@@ -360,33 +346,6 @@ def validate_vanilla_reservation(dungeon, world, player):
return validate_key_layout(world.key_layout[player][dungeon.name], world, player) return validate_key_layout(world.key_layout[player][dungeon.name], world, player)
def should_delay_processing(enabled_origins, potential_origins, potentials, connections, world, player):
disabled_origins = potential_origins.difference(set(enabled_origins))
main_targets = []
for do in disabled_origins:
region = world.get_region(do, player)
portal = _find_portal_for_region(region, world, player)
if portal and not portal.destination:
main_targets.append(region)
if not main_targets:
return False # No non-destination disabled origins found
enabling_regions = {connections[r.name] for r in main_targets}
for enabling_region in enabling_regions:
dungeon_names = {
portal.door.entrance.parent_region.dungeon.name
for dungeon_r in potentials[enabling_region]
if (portal := _find_portal_for_region(world.get_region(dungeon_r, player), world, player))
}
if len(dungeon_names) > 1:
return True
return False
def _find_portal_for_region(region, world, player):
return next((p for ent in region.entrances
if (p := world.get_portal_unsafe(ent.parent_region.name.rstrip(' Portal'), player))), None)
# some useful functions # some useful functions
oppositemap = { oppositemap = {
Direction.South: Direction.North, Direction.South: Direction.North,
@@ -1600,11 +1559,7 @@ def assign_cross_keys(dungeon_builders, world, player):
if actual_chest_keys == 0: if actual_chest_keys == 0:
dungeon.small_keys = [] dungeon.small_keys = []
else: else:
extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100) dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
logger.debug(f'Adding {extra_keys} extra small keys to {name}')
dungeon.extra_small_keys = extra_keys
created_keys = actual_chest_keys + extra_keys
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * created_keys
logger.info(f'{world.fish.translate("cli", "cli", "keydoor.shuffle.time.crossed")}: {time.process_time()-start}') logger.info(f'{world.fish.translate("cli", "cli", "keydoor.shuffle.time.crossed")}: {time.process_time()-start}')
@@ -2176,10 +2131,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_
if actual_chest_keys == 0: if actual_chest_keys == 0:
dungeon.small_keys = [] dungeon.small_keys = []
else: else:
extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100) dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(actual_chest_keys)]
dungeon.extra_small_keys = extra_keys
created_keys = actual_chest_keys + extra_keys
dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(created_keys)]
for name, small_list in small_map.items(): for name, small_list in small_map.items():
used_doors.update(flatten_pair_list(small_list)) used_doors.update(flatten_pair_list(small_list))
@@ -3769,7 +3721,7 @@ logical_connections = [
('PoD Pit Room Block Path S', 'PoD Pit Room'), ('PoD Pit Room Block Path S', 'PoD Pit Room'),
('PoD Arena Landing Bonk Path', 'PoD Arena Bridge'), ('PoD Arena Landing Bonk Path', 'PoD Arena Bridge'),
('PoD Arena North Drop Down', 'PoD Arena Main'), ('PoD Arena North Drop Down', 'PoD Arena Main'),
('PoD Arena Bridge Drop Down', 'PoD Arena Landing'), ('PoD Arena Bridge Drop Down', 'PoD Arena Main'),
('PoD Arena North to Landing Barrier - Orange', 'PoD Arena Landing'), ('PoD Arena North to Landing Barrier - Orange', 'PoD Arena Landing'),
('PoD Arena Main to Ranged Crystal', 'PoD Arena Main - Ranged Crystal'), ('PoD Arena Main to Ranged Crystal', 'PoD Arena Main - Ranged Crystal'),
('PoD Arena Main to Landing Barrier - Blue', 'PoD Arena Landing'), ('PoD Arena Main to Landing Barrier - Blue', 'PoD Arena Landing'),
+12 -43
View File
@@ -117,7 +117,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
for attempt in range(15): for attempt in range(15):
try: try:
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.prizeshuffle[player] == 'nearby' and world.algorithm != 'vanilla_fill': if world.prizeshuffle[player] == 'nearby':
dungeon_pool = [] dungeon_pool = []
for dungeon in world.dungeons: for dungeon in world.dungeons:
from Dungeons import dungeon_table from Dungeons import dungeon_table
@@ -149,14 +149,6 @@ def fill_dungeons_restrictive(world, shuffled_locations):
else: else:
raise FillError(f'Unable to place dungeon prizes: {", ".join(list(map(lambda d: d.name, prizes)))}') raise FillError(f'Unable to place dungeon prizes: {", ".join(list(map(lambda d: d.name, prizes)))}')
if world.algorithm == 'vanilla_fill':
for prize in prizes_copy:
if prize.is_near_dungeon_item(world):
if prize.location and prize.location.parent_region.dungeon:
dungeon = prize.location.parent_region.dungeon
dungeon.prize = prize
prize.dungeon_object = dungeon
random.shuffle(shuffled_locations) random.shuffle(shuffled_locations)
fill(all_state_base, others, shuffled_locations) fill(all_state_base, others, shuffled_locations)
@@ -248,11 +240,9 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
test_state.sweep_for_events() test_state.sweep_for_events()
if location.can_fill(test_state, item_to_place, perform_access_check): if location.can_fill(test_state, item_to_place, perform_access_check):
if valid_key_placement(item_to_place, location, key_pool, test_state, world): if valid_key_placement(item_to_place, location, key_pool, test_state, world):
if (item_to_place.prize and (world.prizeshuffle[item_to_place.player] == 'none' \ if (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none') \
or (world.algorithm == 'vanilla_fill' and item_to_place.is_near_dungeon_item(world)))) \
or valid_dungeon_placement(item_to_place, location, world): or valid_dungeon_placement(item_to_place, location, world):
return location return location
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize:
location.item = None location.item = None
location.event = False location.event = False
@@ -312,9 +302,7 @@ def valid_dungeon_placement(item, location, world):
dungeon = check_dungeon dungeon = check_dungeon
if dungeon: if dungeon:
layout = world.dungeon_layouts[location.player][dungeon.name] layout = world.dungeon_layouts[location.player][dungeon.name]
if item.event: if not is_dungeon_item(item, world) or item.player != location.player:
return True
elif not is_dungeon_item(item, world) or item.player != location.player:
if item.prize and item.is_near_dungeon_item(world): if item.prize and item.is_near_dungeon_item(world):
return item.dungeon_object == dungeon and layout.free_items > 0 return item.dungeon_object == dungeon and layout.free_items > 0
return layout.free_items > 0 return layout.free_items > 0
@@ -348,10 +336,6 @@ def track_dungeon_items(item, location, world):
if item.prize: if item.prize:
location.parent_region.dungeon.prize = item location.parent_region.dungeon.prize = item
item.dungeon_object = location.parent_region.dungeon item.dungeon_object = location.parent_region.dungeon
elif world.algorithm == 'vanilla_fill' and item.prize and location.parent_region.dungeon:
dungeon = location.parent_region.dungeon
dungeon.prize = item
item.dungeon_object = dungeon
def is_dungeon_item(item, world): def is_dungeon_item(item, world):
@@ -744,13 +728,8 @@ def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count
fill_count = len(pot_fill_locations[player]) - flex fill_count = len(pot_fill_locations[player]) - flex
if fill_count > 0: if fill_count > 0:
first_count = min(fill_count, len(pot_item_pool[player])) fill_spots = random.sample(pot_fill_locations[player], fill_count)
fill_items = random.sample(pot_item_pool[player], first_count) fill_items = random.sample(pot_item_pool[player], fill_count)
remaining = fill_count - first_count
if remaining > 0:
other_items = [i for i in item_pool if i.player == player and i not in pot_item_pool[player]]
fill_items += random.sample(other_items, min(remaining, len(other_items)))
fill_spots = random.sample(pot_fill_locations[player], len(fill_items))
for x in fill_items: for x in fill_items:
item_pool.remove(x) item_pool.remove(x)
for x in fill_spots: for x in fill_spots:
@@ -1051,10 +1030,7 @@ def balance_money_progression(world):
'Rupees (100)': 100, 'Rupees (300)': 300} 'Rupees (100)': 100, 'Rupees (300)': 300}
rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90, rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90,
'TR Rupees': 270, 'PoD Dark Basement': 270} 'TR Rupees': 270, 'PoD Dark Basement': 270}
acceptable_balancers = ['Single Bomb', 'Bombs (3)', 'Bombs (10)', acceptable_balancers = ['Bombs (3)', 'Arrows (10)', 'Bombs (10)']
'Single Arrow', 'Arrows (5)', 'Arrows (10)',
'Small Magic', 'Big Magic', 'Small Heart',
'Fairy', 'Chicken', 'Nothing']
base_value = sum(rupee_rooms.values()) base_value = sum(rupee_rooms.values())
available_money = {player: base_value for player in range(1, world.players+1)} available_money = {player: base_value for player in range(1, world.players+1)}
@@ -1084,8 +1060,6 @@ def balance_money_progression(world):
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)] return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
def interesting_item(location, item, world, player): def interesting_item(location, item, world, player):
if location.event or location.locked:
return True
if item.advancement: if item.advancement:
return True return True
if item.type is not None or item.name.startswith('Rupee'): if item.type is not None or item.name.startswith('Rupee'):
@@ -1133,7 +1107,7 @@ def balance_money_progression(world):
slot = shop_to_location_table[location.parent_region.name].index(location.name) slot = shop_to_location_table[location.parent_region.name].index(location.name)
shop = location.parent_region.shop shop = location.parent_region.shop
shop_item = shop.inventory[slot] shop_item = shop.inventory[slot]
if shop_item and location.item and interesting_item(location, location.item, world, location.item.player): if location.item and interesting_item(location, location.item, world, location.item.player):
if location.item.name.startswith('Rupee') and loc_player == location.item.player: if location.item.name.startswith('Rupee') and loc_player == location.item.player:
if shop_item['price'] < rupee_chart[location.item.name]: if shop_item['price'] < rupee_chart[location.item.name]:
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block
@@ -1155,11 +1129,10 @@ def balance_money_progression(world):
unchecked_locations.remove(location) unchecked_locations.remove(location)
if location.item: if location.item:
if location.item.name.startswith('Rupee'): if location.item.name.startswith('Rupee'):
if not (location.item.name == 'Rupee (1)' and world.algorithm != 'district'): wallet[location.item.player] += rupee_chart[location.item.name]
wallet[location.item.player] += rupee_chart[location.item.name] if location.item.name != 'Rupees (300)':
if location.item.name != 'Rupees (300)': balance_locations[location.item.player].add(location)
balance_locations[location.item.player].add(location) if interesting_item(location, location.item, world, location.item.player):
elif interesting_item(location, location.item, world, location.item.player):
checked_locations.append(location) checked_locations.append(location)
elif location.item.name in acceptable_balancers: elif location.item.name in acceptable_balancers:
balance_locations[location.item.player].add(location) balance_locations[location.item.player].add(location)
@@ -1203,11 +1176,7 @@ def balance_money_progression(world):
if len(increase_targets) == 0: if len(increase_targets) == 0:
increase_targets = [x for x in balance_locations[target_player] if (rupee_chart[x.item.name] if x.item.name in rupee_chart else 0) < best_value] increase_targets = [x for x in balance_locations[target_player] if (rupee_chart[x.item.name] if x.item.name in rupee_chart else 0) < best_value]
if len(increase_targets) == 0: if len(increase_targets) == 0:
if state.can_farm_rupees(target_player): raise Exception('No early sphere swaps for rupees - money grind would be required - bailing for now')
logger.warning(f'No more swap targets available. Short by {difference} rupees, but continuing (player can farm)')
break
else:
raise Exception(f'No early sphere swaps for rupees - money grind would be required - bailing for now')
best_target = min(increase_targets, key=lambda t: rupee_chart[t.item.name] if t.item.name in rupee_chart else 0) best_target = min(increase_targets, key=lambda t: rupee_chart[t.item.name] if t.item.name in rupee_chart else 0)
make_item_free = wallet[target_player] < 20 make_item_free = wallet[target_player] < 20
old_value = 0 if make_item_free else (rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0) old_value = 0 if make_item_free else (rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0)
+41 -100
View File
@@ -43,8 +43,8 @@ alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod
progressivegloves = ['Progressive Glove'] * 2 progressivegloves = ['Progressive Glove'] * 2
basicgloves = ['Power Glove', 'Titans Mitts'] basicgloves = ['Power Glove', 'Titans Mitts']
normalbottles = ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)'] normalbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)']
hardbottles = ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)'] hardbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)']
normalbaseitems = (['Magic Upgrade (1/2)', 'Single Arrow', 'Sanctuary Heart Container', 'Arrows (10)', 'Bombs (10)'] + normalbaseitems = (['Magic Upgrade (1/2)', 'Single Arrow', 'Sanctuary Heart Container', 'Arrows (10)', 'Bombs (10)'] +
['Rupees (300)'] * 4 + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 24) ['Rupees (300)'] * 4 + ['Boss Heart Container'] * 10 + ['Piece of Heart'] * 24)
@@ -325,42 +325,6 @@ def generate_itempool(world, player):
for _ in range(0, amt): for _ in range(0, amt):
pool.append('Rupees (20)') pool.append('Rupees (20)')
if world.shopsanity[player] and not skip_pool_adjustments:
for shop in world.shops[player]:
if shop.region.name in shop_to_location_table:
for index, slot in enumerate(shop.inventory):
if slot:
item = slot['item']
if shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal':
pool.append('Rupees (20)')
else:
pool.append(item)
if (world.customizer and world.customizer.get_item_pool_adjust()
and player in world.customizer.get_item_pool_adjust()):
diff = difficulties[world.difficulty[player]]
for item_name, delta in world.customizer.get_item_pool_adjust()[player].items():
if not isinstance(delta, int):
continue
if delta > 0:
if item_name == 'Bottle (Random)':
for _ in range(delta):
pool.append(random.choice(diff.bottles))
else:
pool.extend([item_name] * delta)
elif delta < 0:
remove_count = abs(delta)
if item_name == 'Bottle (Random)':
bottle_names = set(diff.bottles)
for _ in range(remove_count):
bottle = next((x for x in pool if x in bottle_names), None)
if bottle:
pool.remove(bottle)
else:
for _ in range(remove_count):
if item_name in pool:
pool.remove(item_name)
if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave'] \ if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave'] \
and world.keyshuffle[player] not in ['none', 'nearby']: and world.keyshuffle[player] not in ['none', 'nearby']:
# In HMG force swamp smalls in pots to allow getting out of swamp palace # In HMG force swamp smalls in pots to allow getting out of swamp palace
@@ -372,13 +336,6 @@ def generate_itempool(world, player):
world.push_precollected(ItemFactory(item, player)) world.push_precollected(ItemFactory(item, player))
if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player): if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player):
if world.customizer:
placements = world.customizer.get_placements()
if placements:
custom_uncle = placements.get(player, {}).get("Link's Uncle")
if custom_uncle:
placed_items["Link's Uncle"] = custom_uncle
if "Link's Uncle" not in placed_items: if "Link's Uncle" not in placed_items:
found_sword = False found_sword = False
found_bow = False found_bow = False
@@ -407,10 +364,20 @@ def generate_itempool(world, player):
world.escape_assist[player].append('bombs') world.escape_assist[player].append('bombs')
for (location, item) in placed_items.items(): for (location, item) in placed_items.items():
loc = world.get_location(location, player) world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
world.push_item(loc, ItemFactory(item, player), False) world.get_location(location, player).event = True
loc.event = True world.get_location(location, player).locked = True
loc.locked = True
if world.shopsanity[player] and not skip_pool_adjustments:
for shop in world.shops[player]:
if shop.region.name in shop_to_location_table:
for index, slot in enumerate(shop.inventory):
if slot:
item = slot['item']
if shop.region.name == 'Capacity Upgrade' and world.difficulty[player] != 'normal':
pool.append('Rupees (20)')
else:
pool.append(item)
items = ItemFactory(pool, player) items = ItemFactory(pool, player)
if world.shopsanity[player]: if world.shopsanity[player]:
@@ -496,11 +463,11 @@ def generate_itempool(world, player):
# shuffle bottle refills # shuffle bottle refills
if world.difficulty[player] in ['hard', 'expert']: if world.difficulty[player] in ['hard', 'expert']:
waterfall_bottle = hardbottles[random.randint(0, 4)] waterfall_bottle = hardbottles[random.randint(0, 5)]
pyramid_bottle = hardbottles[random.randint(0, 4)] pyramid_bottle = hardbottles[random.randint(0, 5)]
else: else:
waterfall_bottle = normalbottles[random.randint(0, 5)] waterfall_bottle = normalbottles[random.randint(0, 6)]
pyramid_bottle = normalbottles[random.randint(0, 5)] pyramid_bottle = normalbottles[random.randint(0, 6)]
world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle) world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle)
set_up_shops(world, player) set_up_shops(world, player)
@@ -872,7 +839,7 @@ def set_up_shops(world, player):
rss.locked = True rss.locked = True
cap_shop = world.get_region('Capacity Upgrade', player).shop cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.name != "Red Shield Shop" and s.region.player == player], 5): for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.player == player], 5):
shop.custom = True shop.custom = True
shop.locked = True shop.locked = True
if retro_bow: if retro_bow:
@@ -944,7 +911,6 @@ def customize_shops(world, player):
rep_price = final_price(loc.name, item.price, world, player) rep_price = final_price(loc.name, item.price, world, player)
shop.add_inventory(idx, upgrade.name, up_price, 6, shop.add_inventory(idx, upgrade.name, up_price, 6,
item.name, rep_price, player=item.player) item.name, rep_price, player=item.player)
upgrade.world = loc.item.world
loc.item = upgrade loc.item = upgrade
upgrade.location = loc upgrade.location = loc
if not found_arrow_upgrade and len(possible_replacements) > 0: if not found_arrow_upgrade and len(possible_replacements) > 0:
@@ -960,7 +926,6 @@ def customize_shops(world, player):
shop.add_inventory(idx, upgrade.name, up_price, 6, shop.add_inventory(idx, upgrade.name, up_price, 6,
item.name, rep_price, player=item.player) item.name, rep_price, player=item.player)
loc.item = upgrade loc.item = upgrade
upgrade.world = loc.item.world
upgrade.location = loc upgrade.location = loc
change_shop_items_to_rupees(world, player, shops_to_customize) change_shop_items_to_rupees(world, player, shops_to_customize)
balance_prices(world, player) balance_prices(world, player)
@@ -997,11 +962,9 @@ def change_shop_items_to_rupees(world, player, shops):
for location in locations: for location in locations:
if location.item.name in shop_transfer.keys() and (location.parent_region.name not in shops or location.name == 'Potion Shop'): if location.item.name in shop_transfer.keys() and (location.parent_region.name not in shops or location.name == 'Potion Shop'):
new_item = ItemFactory(shop_transfer[location.item.name], location.item.player) new_item = ItemFactory(shop_transfer[location.item.name], location.item.player)
new_item.world = location.item.world
location.item = new_item location.item = new_item
if location.parent_region.name == 'Capacity Upgrade' and location.item.name in cap_blacklist: if location.parent_region.name == 'Capacity Upgrade' and location.item.name in cap_blacklist:
new_item = ItemFactory('Rupees (300)', location.item.player) new_item = ItemFactory('Rupees (300)', location.item.player)
new_item.world = location.item.world
location.item = new_item location.item = new_item
shop = world.get_region('Capacity Upgrade', player).shop shop = world.get_region('Capacity Upgrade', player).shop
slot = shop_to_location_table['Capacity Upgrade'].index(location.name) slot = shop_to_location_table['Capacity Upgrade'].index(location.name)
@@ -1544,7 +1507,7 @@ def make_customizer_pool(world, player):
elif timer == 'ohko': elif timer == 'ohko':
clock_mode = 'ohko' clock_mode = 'ohko'
if world.goal[player] in ['sanctuary']: if goal in ['sanctuary']:
place_item('Sanctuary', 'Triforce') place_item('Sanctuary', 'Triforce')
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00: if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00:
place_item('Master Sword Pedestal', 'Nothing') place_item('Master Sword Pedestal', 'Nothing')
@@ -1680,15 +1643,12 @@ def fill_specific_items(world):
item_to_place, event_flag = get_item_and_event_flag(item, world, player, item_to_place, event_flag = get_item_and_event_flag(item, world, player,
dungeon_pool, prize_set, prize_pool) dungeon_pool, prize_set, prize_pool)
if item_to_place: if item_to_place:
if not loc.item: world.push_item(loc, item_to_place, False)
world.push_item(loc, item_to_place, False) loc.locked = True
loc.locked = True track_outside_keys(item_to_place, loc, world)
track_outside_keys(item_to_place, loc, world) track_dungeon_items(item_to_place, loc, world)
track_dungeon_items(item_to_place, loc, world) loc.event = (event_flag or item_to_place.advancement
loc.event = (event_flag or item_to_place.advancement or item_to_place.bigkey or item_to_place.smallkey)
or item_to_place.bigkey or item_to_place.smallkey)
elif loc.item != item_to_place:
logging.getLogger('').warning("Failed to place item %s at location %s because it already contained %s", item_to_place, loc.name, loc.item)
else: else:
raise Exception(f'Did not find "{item}" in item pool to place at "{location}"') raise Exception(f'Did not find "{item}" in item pool to place at "{location}"')
advanced_placements = world.customizer.get_advanced_placements() advanced_placements = world.customizer.get_advanced_placements()
@@ -1767,7 +1727,6 @@ def shuffle_event_items(world, player):
available_quests = follower_quests.copy() available_quests = follower_quests.copy()
available_pickups = [quests[0] for quests in available_quests.values()] available_pickups = [quests[0] for quests in available_quests.values()]
# finalize customizer followers first
for loc_name in follower_quests.keys(): for loc_name in follower_quests.keys():
loc = world.get_location(loc_name, player) loc = world.get_location(loc_name, player)
if loc.item: if loc.item:
@@ -1775,6 +1734,7 @@ def shuffle_event_items(world, player):
available_quests.pop(loc_name) available_quests.pop(loc_name)
available_pickups.remove(loc.item.name) available_pickups.remove(loc.item.name)
if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups: if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups:
zelda_dropoff = 'Zelda Pickup' zelda_dropoff = 'Zelda Pickup'
if world.default_zelda_region[player] == 'Thieves Blind\'s Cell': if world.default_zelda_region[player] == 'Thieves Blind\'s Cell':
@@ -1784,39 +1744,20 @@ def shuffle_event_items(world, player):
available_pickups.remove(zelda_pickup) available_pickups.remove(zelda_pickup)
set_event_item(world, player, zelda_dropoff, zelda_pickup) set_event_item(world, player, zelda_dropoff, zelda_pickup)
follower_locations = [world.get_location(loc_name, player) for loc_name in available_quests.keys()] random.shuffle(available_pickups)
attempts = 10 restricted_pickups = { 'Get Frog': 'Dark Blacksmith Ruins'}
for attempt in range(attempts): for pickup in restricted_pickups:
try: restricted_quests = [q for q in available_quests.keys() if q not in restricted_pickups[pickup]]
all_state = world.get_all_state(keys=True) random.shuffle(restricted_quests)
if world.prizeshuffle[player] != 'wild': quest = restricted_quests.pop()
from Items import prize_item_table available_quests.pop(quest)
prizes = ItemFactory(list(prize_item_table.keys()), player) available_pickups.remove(pickup)
for prize in prizes: set_event_item(world, player, quest, pickup)
all_state.collect(prize, True)
# randomize the follower pickups, but ensure that the last items are the unrestrictive ones for pickup in available_pickups:
unrestrictive_pickups = ItemFactory([p for p in ['Zelda Herself', 'Sign Vandalized'] if p in available_pickups], player) quest, _ = available_quests.popitem()
restrictive_pickups = ItemFactory([p for p in available_pickups if p not in unrestrictive_pickups], player) set_event_item(world, player, quest, pickup)
random.shuffle(restrictive_pickups)
random.shuffle(unrestrictive_pickups)
pickup_items = unrestrictive_pickups + restrictive_pickups
random.shuffle(follower_locations)
fill_restrictive(world, all_state, follower_locations, pickup_items, single_player_placement=True)
for loc_name in available_quests.keys():
loc = world.get_location(loc_name, player)
if loc.item:
set_event_item(world, player, loc_name)
except FillError as e:
logging.getLogger('').info("Failed to place followers (%s). Will retry %s more times", e, attempts - attempt - 1)
for loc in follower_locations:
loc.item = None
continue
break
else:
raise FillError(f'Unable to place followers: {", ".join(list(map(lambda f: f.name, pickup_items)))}')
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
+7 -8
View File
@@ -55,7 +55,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class
'Master Sword': (True, False, 'Sword', 0x50, 100, 'Master Sword!\nEvil\'s bane!', '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, 'Master Sword!\nEvil\'s bane!', '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, 'Tempered Sword!\nMore slashy!', '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, 'Tempered Sword!\nMore slashy!', '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, 'Fighter Sword!\nStarter level slashy!', '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, 'Fighter Sword!\nStarter level slashy!', '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, 50, 'Sword and Shield!\nUncle sword ahoy!', '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'), 'Sword and Shield': (True, False, 'Sword', 0x00, 'Sword and Shield!\nUncle sword ahoy!', '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, 'Golden Sword!\nBest slashy!', '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, 'Golden Sword!\nBest slashy!', '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, 'Sword!\nA better sword for 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, 'Sword!\nA better sword for 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, 'Glove!\nLift more than you can now!', '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, 'Glove!\nLift more than you can now!', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'),
@@ -97,8 +97,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class
'Progressive Shield': (True, False, None, 0x5F, 50, 'Shield!\nA better shield for your time!', '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, 'Shield!\nA better shield for your time!', '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, 'Bug Net!\nCatch all manner\nof things!', '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, 'Bug Net!\nCatch all manner\nof things!', '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, 'Cane of Byrna!\nSwirly protection!', '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, 'Cane of Byrna!\nSwirly protection!', '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, True, None, 0x3E, 40, 'Heart Container!\nHealth Increased!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart container'), 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Heart Container!\nHealth Increased!', '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, 'Heart Container!\nHealth Increased!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart container'), 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Heart Container!\nHealth Increased!', '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, 'Heart Piece!\nOne step closer\nto more health!', '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, 'Heart Piece!\nOne step closer\nto more health!', '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, 'Rupees!\nJust pocket\nchange.', '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, 'Rupees!\nJust pocket\nchange.', '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, 'Rupees!\nJust pocket\nchange.', '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, 'Rupees!\nJust pocket\nchange.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'),
@@ -134,9 +134,9 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 60, 'A big key for\nAgahnim\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'), 'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 60, 'A big key for\nAgahnim\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 10, 'A compass for\nAgahnim\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'), 'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 10, 'A compass for\nAgahnim\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'), 'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nPalace of Darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nDark Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nPalace of Darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nDark Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nPalace of Darkness', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nDark Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'), 'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'),
'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key for\nThieves Town', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'), 'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key for\nThieves Town', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'),
'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key for\nThieves Town', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'), 'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key for\nThieves Town', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'),
@@ -172,10 +172,9 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class
'Red Potion': (False, False, None, 0x2E, 150, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'), 'Red Potion': (False, False, None, 0x2E, 150, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
'Green Potion': (False, False, None, 0x2F, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'), 'Green Potion': (False, False, None, 0x2F, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
'Blue Potion': (False, False, None, 0x30, 160, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'), 'Blue Potion': (False, False, None, 0x30, 160, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
'Orange Potion': (False, False, None, 0x66, 1000, 'Energizing orange goop!', 'and the orange goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has orange goo again', 'an orange potion'),
'Bee': (False, False, None, 0x0E, 10, '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 bee'), 'Bee': (False, False, None, 0x0E, 10, '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 bee'),
'Good Bee': (False, False, None, 0xD6, 10, 'I will sting your foes a lot', 'and the cold buddy', 'the beekeeper kid', 'cold insect for sale', 'shroom pollenation', 'bottle boy has cold bee again', 'a good bee'), 'Good Bee': (False, False, None, 0xD6, 10, 'I will sting your foes a lot', 'and the cold buddy', 'the beekeeper kid', 'cold insect for sale', 'shroom pollenation', 'bottle boy has cold bee again', 'a good bee'),
'Small Heart': (False, False, None, 0x42, 10, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a refill heart'), 'Small Heart': (False, False, None, 0x42, 10, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
'Apples': (False, False, None, 0xD1, 30, 'Just a few pieces of fruit!', 'and the juicy fruit', 'the fruity kid', 'the fruit stand', 'expired fruit', 'bottle boy has fruit again', 'an apple hoard'), 'Apples': (False, False, None, 0xD1, 30, 'Just a few pieces of fruit!', 'and the juicy fruit', 'the fruity kid', 'the fruit stand', 'expired fruit', 'bottle boy has fruit again', 'an apple hoard'),
'Fairy': (False, False, None, 0xD2, 50, 'Just a pixie!', 'and the pixie', 'the pixie kid', 'pixie for sale', 'pixie fungus', 'bottle boy has pixie again', 'a pixie'), 'Fairy': (False, False, None, 0xD2, 50, 'Just a pixie!', 'and the pixie', 'the pixie kid', 'pixie for sale', 'pixie fungus', 'bottle boy has pixie again', 'a pixie'),
'Beat Boss': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Beat Boss': (True, False, 'Event', 999, None, None, None, None, None, None, None, None),
+7 -41
View File
@@ -50,6 +50,7 @@ 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 OverworldShuffle import ( from OverworldShuffle import (
create_dynamic_flute_exits,
create_dynamic_mirror_exits, create_dynamic_mirror_exits,
link_overworld, link_overworld,
update_world_regions, update_world_regions,
@@ -67,7 +68,6 @@ from Rom import (
JsonRom, JsonRom,
LocalRom, LocalRom,
apply_rom_settings, apply_rom_settings,
apply_rom_patches,
get_hash_string, get_hash_string,
patch_race_rom, patch_race_rom,
patch_rom, patch_rom,
@@ -86,7 +86,6 @@ from source.item.FillUtil import (
verify_item_pool_config, verify_item_pool_config,
) )
from source.overworld.EntranceShuffle2 import link_entrances_new from source.overworld.EntranceShuffle2 import link_entrances_new
from source.overworld.FluteShuffle import create_dynamic_flute_exits
from source.rom.DataTables import init_data_tables from source.rom.DataTables import init_data_tables
from source.tools.BPS import create_bps_from_data from source.tools.BPS import create_bps_from_data
from UnderworldGlitchRules import ( from UnderworldGlitchRules import (
@@ -336,29 +335,10 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players+1): for player in range(1, world.players+1):
if world.shopsanity[player]: if world.shopsanity[player]:
customize_shops(world, player) customize_shops(world, player)
if args.algorithm in ['balanced', 'equitable']:
if not args.skip_money_balance and args.algorithm in ['balanced', 'equitable']:
balance_money_progression(world) balance_money_progression(world)
ensure_good_items(world, True) ensure_good_items(world, True)
for player in range(1, world.players+1):
if args.orange_potion and world.difficulty[player] == "normal":
cap_shop = world.get_region('Capacity Upgrade', player).shop
potion = {
'item': "Orange Potion",
'price': 500,
'max': 0,
'replacement': None,
'replacement_price': 0,
'create_location': False,
'player': 0,
}
if cap_shop.inventory[0] is None:
cap_shop.inventory[0] = potion
else:
cap_shop.inventory.insert(1, potion)
if args.print_custom_yaml: if args.print_custom_yaml:
world.settings.record_info(world) world.settings.record_info(world)
world.settings.record_overworld(world) world.settings.record_overworld(world)
@@ -384,8 +364,6 @@ def main(args, seed=None, fish=None):
rom_names.append((player, team, list(rom.name))) rom_names.append((player, team, list(rom.name)))
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash) world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
if args.patches[player]:
apply_rom_patches(rom, map(lambda arg: arg.strip(), args.patches[player].split(",")))
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.triforce_gfx[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.triforce_gfx[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],
@@ -501,7 +479,7 @@ def export_yaml(args, fish):
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]] world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
set_starting_inventory(world, args) set_starting_inventory(world, args)
world.settings = CustomSettings() world.settings = CustomSettings()
@@ -525,7 +503,7 @@ def init_world(args, fish):
customized.load_yaml(args.customizer) customized.load_yaml(args.customizer)
customized.adjust_args(args, False) customized.adjust_args(args, False)
world = World(args.multi, args.ow_layout, args.ow_parallel, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler)
@@ -538,7 +516,6 @@ def init_world(args, fish):
world.bigkeyshuffle = args.bigkeyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.prizeshuffle = args.prizeshuffle.copy() world.prizeshuffle = args.prizeshuffle.copy()
world.showloot = args.showloot.copy() world.showloot = args.showloot.copy()
world.loothud = args.loothud.copy()
world.showmap = args.showmap.copy() world.showmap = args.showmap.copy()
world.bombbag = args.bombbag.copy() world.bombbag = args.bombbag.copy()
world.flute_mode = args.flute_mode.copy() world.flute_mode = args.flute_mode.copy()
@@ -546,14 +523,12 @@ def init_world(args, fish):
world.crystals_ganon_orig = args.crystals_ganon.copy() world.crystals_ganon_orig = args.crystals_ganon.copy()
world.crystals_gt_orig = args.crystals_gt.copy() world.crystals_gt_orig = args.crystals_gt.copy()
world.ganon_item_orig = args.ganon_item.copy() world.ganon_item_orig = args.ganon_item.copy()
world.require_ganon_item = args.require_ganon_item.copy()
world.bosses_ganon = {player: int(args.bosses_ganon[player]) for player in range(1, world.players + 1)} 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.bosshunt_include_agas = args.bosshunt_include_agas.copy()
world.owTerrain = args.ow_terrain.copy() world.owTerrain = args.ow_terrain.copy()
world.owKeepSimilar = args.ow_keepsimilar.copy() world.owKeepSimilar = args.ow_keepsimilar.copy()
world.owWhirlpoolShuffle = args.ow_whirlpool.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy()
world.owFluteShuffle = args.ow_fluteshuffle.copy() world.owFluteShuffle = args.ow_fluteshuffle.copy()
world.owFog = args.ow_fog.copy()
world.shuffle_followers = args.shuffle_followers.copy() world.shuffle_followers = args.shuffle_followers.copy()
world.shuffle_bonk_drops = args.bonk_drops.copy() world.shuffle_bonk_drops = args.bonk_drops.copy()
world.open_pyramid = args.openpyramid.copy() world.open_pyramid = args.openpyramid.copy()
@@ -588,14 +563,13 @@ def init_world(args, fish):
world.damage_challenge = args.damage_challenge.copy() world.damage_challenge = args.damage_challenge.copy()
world.shuffle_damage_table = args.shuffle_damage_table.copy() world.shuffle_damage_table = args.shuffle_damage_table.copy()
world.crystal_book = args.crystal_book.copy() world.crystal_book = args.crystal_book.copy()
world.extra_keys = {player: int(args.extra_keys[player]) for player in range(1, world.players + 1)}
world.overworld_map = args.overworld_map.copy() world.overworld_map = args.overworld_map.copy()
world.take_any = args.take_any.copy() world.take_any = args.take_any.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.collection_rate = args.collection_rate.copy()
world.colorizepots = args.colorizepots.copy() world.colorizepots = args.colorizepots.copy()
world.aga_randomness = args.aga_randomness.copy() world.aga_randomness = args.aga_randomness.copy()
world.money_balance = {player: int(args.money_balance[player]) for player in range(1, world.players + 1)} world.money_balance = args.money_balance.copy()
# custom settings - these haven't been promoted to full settings yet # custom settings - these haven't been promoted to full settings yet
in_progress_settings = ['force_enemy'] in_progress_settings = ['force_enemy']
@@ -832,7 +806,7 @@ def set_starting_inventory(world, args):
def copy_world(world): def copy_world(world):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owLayout, world.owParallel, 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,
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
ret.teams = world.teams ret.teams = world.teams
@@ -861,7 +835,6 @@ def copy_world(world):
ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.prizeshuffle = world.prizeshuffle.copy() ret.prizeshuffle = world.prizeshuffle.copy()
ret.showloot = world.showloot.copy() ret.showloot = world.showloot.copy()
ret.loothud = world.loothud.copy()
ret.showmap = world.showmap.copy() ret.showmap = world.showmap.copy()
ret.bombbag = world.bombbag.copy() ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy() ret.flute_mode = world.flute_mode.copy()
@@ -872,7 +845,6 @@ def copy_world(world):
ret.bosses_ganon = world.bosses_ganon.copy() ret.bosses_ganon = world.bosses_ganon.copy()
ret.bosshunt_include_agas = world.bosshunt_include_agas.copy() ret.bosshunt_include_agas = world.bosshunt_include_agas.copy()
ret.ganon_item = world.ganon_item.copy() ret.ganon_item = world.ganon_item.copy()
ret.require_ganon_item = world.require_ganon_item.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
ret.crystals_gt_orig = world.crystals_gt_orig.copy() ret.crystals_gt_orig = world.crystals_gt_orig.copy()
ret.ganon_item_orig = world.ganon_item_orig.copy() ret.ganon_item_orig = world.ganon_item_orig.copy()
@@ -880,7 +852,6 @@ def copy_world(world):
ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owKeepSimilar = world.owKeepSimilar.copy()
ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy()
ret.owFluteShuffle = world.owFluteShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy()
ret.owFog = world.owFog.copy()
ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_followers = world.shuffle_followers.copy()
ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy()
ret.open_pyramid = world.open_pyramid.copy() ret.open_pyramid = world.open_pyramid.copy()
@@ -895,7 +866,6 @@ def copy_world(world):
ret.damage_challenge = world.damage_challenge.copy() ret.damage_challenge = world.damage_challenge.copy()
ret.shuffle_damage_table = world.shuffle_damage_table.copy() ret.shuffle_damage_table = world.shuffle_damage_table.copy()
ret.crystal_book = world.crystal_book.copy() ret.crystal_book = world.crystal_book.copy()
ret.extra_keys = world.extra_keys.copy()
ret.overworld_map = world.overworld_map.copy() ret.overworld_map = world.overworld_map.copy()
ret.take_any = world.take_any.copy() ret.take_any = world.take_any.copy()
ret.boss_shuffle = world.boss_shuffle.copy() ret.boss_shuffle = world.boss_shuffle.copy()
@@ -1067,7 +1037,7 @@ def copy_world(world):
def copy_world_premature(world, player, create_flute_exits=True): def copy_world_premature(world, player, create_flute_exits=True):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owLayout, world.owParallel, 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,
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
ret.teams = world.teams ret.teams = world.teams
@@ -1096,7 +1066,6 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.prizeshuffle = world.prizeshuffle.copy() ret.prizeshuffle = world.prizeshuffle.copy()
ret.showloot = world.showloot.copy() ret.showloot = world.showloot.copy()
ret.loothud = world.loothud.copy()
ret.showmap = world.showmap.copy() ret.showmap = world.showmap.copy()
ret.bombbag = world.bombbag.copy() ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy() ret.flute_mode = world.flute_mode.copy()
@@ -1107,7 +1076,6 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.bosses_ganon = world.bosses_ganon.copy() ret.bosses_ganon = world.bosses_ganon.copy()
ret.bosshunt_include_agas = world.bosshunt_include_agas.copy() ret.bosshunt_include_agas = world.bosshunt_include_agas.copy()
ret.ganon_item = world.ganon_item.copy() ret.ganon_item = world.ganon_item.copy()
ret.require_ganon_item = world.require_ganon_item.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
ret.crystals_gt_orig = world.crystals_gt_orig.copy() ret.crystals_gt_orig = world.crystals_gt_orig.copy()
ret.ganon_item_orig = world.ganon_item_orig.copy() ret.ganon_item_orig = world.ganon_item_orig.copy()
@@ -1115,7 +1083,6 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owKeepSimilar = world.owKeepSimilar.copy()
ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy()
ret.owFluteShuffle = world.owFluteShuffle.copy() ret.owFluteShuffle = world.owFluteShuffle.copy()
ret.owFog = world.owFog.copy()
ret.shuffle_followers = world.shuffle_followers.copy() ret.shuffle_followers = world.shuffle_followers.copy()
ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy()
ret.open_pyramid = world.open_pyramid.copy() ret.open_pyramid = world.open_pyramid.copy()
@@ -1130,7 +1097,6 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.damage_challenge = world.damage_challenge.copy() ret.damage_challenge = world.damage_challenge.copy()
ret.shuffle_damage_table = world.shuffle_damage_table.copy() ret.shuffle_damage_table = world.shuffle_damage_table.copy()
ret.crystal_book = world.crystal_book.copy() ret.crystal_book = world.crystal_book.copy()
ret.extra_keys = world.extra_keys.copy()
ret.overworld_map = world.overworld_map.copy() ret.overworld_map = world.overworld_map.copy()
ret.take_any = world.take_any.copy() ret.take_any = world.take_any.copy()
ret.boss_shuffle = world.boss_shuffle.copy() ret.boss_shuffle = world.boss_shuffle.copy()
-8
View File
@@ -326,20 +326,12 @@ def create_owedges(world, player):
world.owedges += edges world.owedges += edges
world.initialize_owedges(edges) world.initialize_owedges(edges)
set_parallel_owedge_links(world, player, edges)
def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff): def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
if name not in OWExitTypes['OWEdge']: if name not in OWExitTypes['OWEdge']:
OWExitTypes['OWEdge'].append(name) OWExitTypes['OWEdge'].append(name)
return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex) return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex)
def set_parallel_owedge_links(world, player, edges):
for edge in edges:
if edge.name in parallel_links:
dw_edge = world.get_owedge(parallel_links[edge.name], player)
edge.parallel = dw_edge
dw_edge.parallel = edge
OWEdgeGroups = { OWEdgeGroups = {
#(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup) #(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup)
+2 -2
View File
@@ -509,7 +509,7 @@ boots_clips = [
mirror_clips_local = [ mirror_clips_local = [
('Desert East Mirror Clip', 'Mire Area', 'Desert Mouth'), ('Desert East Mirror Clip', 'Mire Area', 'Desert Mouth'),
('EDDM Bridge Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)'), ('EDDM Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)'),
('EDDM Mirror Clip', 'East Dark Death Mountain (Top)', 'Dark Death Mountain Ledge') ('EDDM Mirror Clip', 'East Dark Death Mountain (Top)', 'Dark Death Mountain Ledge')
] ]
@@ -520,4 +520,4 @@ mirror_clips = [
mirror_offsets = [ mirror_offsets = [
(['DM Offset Mirror', 'DDM Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Courtyard Northeast', 'Pyramid Crack'], ['Pyramid Area', 'Hyrule Castle Courtyard']), (['DM Offset Mirror', 'DDM Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Courtyard Northeast', 'Pyramid Crack'], ['Pyramid Area', 'Hyrule Castle Courtyard']),
(['DM To HC Ledge Offset Mirror', 'DDM To Pyramid Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', 'Pyramid Area'], ['Pyramid Area', 'Hyrule Castle Area']) (['DM To HC Ledge Offset Mirror', 'DDM To Pyramid Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', 'Pyramid Area'], ['Pyramid Area', 'Hyrule Castle Area'])
] ]
+289 -179
View File
@@ -25,11 +25,6 @@ from OWEdges import (
) )
from Regions import mark_light_dark_world_regions from Regions import mark_light_dark_world_regions
from source.overworld.EntranceShuffle2 import connect_simple from source.overworld.EntranceShuffle2 import connect_simple
from source.overworld.FluteShuffle import (
default_flute_connections,
flute_data,
shuffle_flute_spots,
)
from Utils import bidict from Utils import bidict
parallel_links_new = None # needs to be globally available, reset every new generation/player parallel_links_new = None # needs to be globally available, reset every new generation/player
@@ -132,38 +127,37 @@ def link_overworld(world, player):
# restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship # restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship
parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken) parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken)
if world.owLayout[player] != 'grid': if world.owKeepSimilar[player]:
if world.owKeepSimilar[player]: del parallel_links_new['Maze Race ES']
del parallel_links_new['Maze Race ES'] del parallel_links_new['Kakariko Suburb WS']
del parallel_links_new['Kakariko Suburb WS'] for group in trimmed_groups.keys():
for group in trimmed_groups.keys(): (std, region, axis, terrain, parallel, _, custom) = group
(std, region, axis, terrain, parallel, _, custom) = group if parallel == IsParallel.Yes:
if parallel == IsParallel.Yes:
(forward_edges, back_edges) = trimmed_groups[group]
if ['Maze Race ES'] in forward_edges:
forward_edges.remove(['Maze Race ES'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES'])
if ['Kakariko Suburb WS'] in back_edges:
back_edges.remove(['Kakariko Suburb WS'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS'])
trimmed_groups[group] = (forward_edges, back_edges)
else:
for group in trimmed_groups.keys():
(std, region, axis, terrain, _, _, custom) = group
(forward_edges, back_edges) = trimmed_groups[group] (forward_edges, back_edges) = trimmed_groups[group]
if ['Dig Game EC', 'Dig Game ES'] in forward_edges: if ['Maze Race ES'] in forward_edges:
forward_edges.remove(['Dig Game EC', 'Dig Game ES']) forward_edges.remove(['Maze Race ES'])
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES']) trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC']) if ['Kakariko Suburb WS'] in back_edges:
if ['Frog WC', 'Frog WS'] in back_edges: back_edges.remove(['Kakariko Suburb WS'])
back_edges.remove(['Frog WC', 'Frog WS']) trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS'])
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC'])
trimmed_groups[group] = (forward_edges, back_edges) trimmed_groups[group] = (forward_edges, back_edges)
else:
for group in trimmed_groups.keys():
(std, region, axis, terrain, _, _, custom) = group
(forward_edges, back_edges) = trimmed_groups[group]
if ['Dig Game EC', 'Dig Game ES'] in forward_edges:
forward_edges.remove(['Dig Game EC', 'Dig Game ES'])
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC'])
if ['Frog WC', 'Frog WS'] in back_edges:
back_edges.remove(['Frog WC', 'Frog WS'])
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS'])
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC'])
trimmed_groups[group] = (forward_edges, back_edges)
parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})} parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})}
connected_edges = [] connected_edges = []
if world.owLayout[player] != 'vanilla': if world.owShuffle[player] != 'vanilla':
trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player) trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player)
trimmed_groups = reorganize_groups(world, trimmed_groups, player) trimmed_groups = reorganize_groups(world, trimmed_groups, player)
@@ -251,7 +245,7 @@ def link_overworld(world, player):
if 'undefined_chance' in custom_crossed: if 'undefined_chance' in custom_crossed:
undefined_chance = custom_crossed['undefined_chance'] undefined_chance = custom_crossed['undefined_chance']
if limited_crossed > -1 and world.owLayout[player] != 'grid': if limited_crossed > -1:
# connect forced crossed non-parallel edges based on previously determined tile flips # connect forced crossed non-parallel edges based on previously determined tile flips
for edge in swapped_edges: for edge in swapped_edges:
if edge not in parallel_links_new: if edge not in parallel_links_new:
@@ -283,10 +277,10 @@ def link_overworld(world, player):
s[0x30], s[0x35], s[0x30], s[0x35],
s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f]) s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player) world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player)
elif (limited_crossed > -1 and world.owLayout[player] != 'grid') or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'): elif limited_crossed > -1 or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
crossed_candidates = list() crossed_candidates = list()
for group in trimmed_groups.keys(): for group in trimmed_groups.keys():
(mode, wrld, _, terrain, parallel, _, _) = group (mode, wrld, dir, terrain, parallel, count, _) = group
if wrld == WorldType.Light and mode != OpenStd.Standard: if wrld == WorldType.Light and mode != OpenStd.Standard:
for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]): for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]):
if forward_set[0] in parallel_links_new: if forward_set[0] in parallel_links_new:
@@ -297,7 +291,7 @@ def link_overworld(world, player):
combine_set = forward_combine+back_combine combine_set = forward_combine+back_combine
skip_forward = False skip_forward = False
if world.owLayout[player] == 'vanilla': if world.owShuffle[player] == 'vanilla':
if any(edge in force_crossed for edge in combine_set): if any(edge in force_crossed for edge in combine_set):
if not any(edge in force_noncrossed for edge in combine_set): if not any(edge in force_noncrossed for edge in combine_set):
if any(edge in force_crossed for edge in forward_combine): if any(edge in force_crossed for edge in forward_combine):
@@ -354,7 +348,6 @@ def link_overworld(world, player):
world.owcrossededges[player].extend(edge_set) world.owcrossededges[player].extend(edge_set)
assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge candidate added to crossed edges" assert len(world.owcrossededges[player]) == len(set(world.owcrossededges[player])), "Same edge candidate added to crossed edges"
# TODO: Someday don't force parallels also? Keeping for now to ensure full reachability
for edge in copy.deepcopy(world.owcrossededges[player]): for edge in copy.deepcopy(world.owcrossededges[player]):
if edge in parallel_links_new: if edge in parallel_links_new:
if parallel_links_new[edge] not in world.owcrossededges[player]: if parallel_links_new[edge] not in world.owcrossededges[player]:
@@ -432,7 +425,7 @@ def link_overworld(world, player):
# layout shuffle # layout shuffle
logging.getLogger('').debug('Shuffling overworld layout') logging.getLogger('').debug('Shuffling overworld layout')
if world.owLayout[player] == 'vanilla': if world.owShuffle[player] == 'vanilla':
# apply outstanding flips # apply outstanding flips
trimmed_groups = performSwap(trimmed_groups, edges_to_swap) trimmed_groups = performSwap(trimmed_groups, edges_to_swap)
assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap) assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap)
@@ -445,15 +438,8 @@ def link_overworld(world, player):
assert len(forward_set) == len(back_set) assert len(forward_set) == len(back_set)
for (forward_edge, back_edge) in zip(forward_set, back_set): for (forward_edge, back_edge) in zip(forward_set, back_set):
connect_two_way(world, forward_edge, back_edge, player, connected_edges) connect_two_way(world, forward_edge, back_edge, player, connected_edges)
elif world.owLayout[player] == 'grid':
from source.overworld.LayoutGenerator import generate_random_grid_layout
for exitname, destname in special_screen_connections:
connect_two_way(world, exitname, destname, player, connected_edges)
generate_random_grid_layout(world, player, connected_edges, ow_crossed_tiles if world.owCrossed[player] == 'grouped' else [], force_noncrossed, force_crossed, limited_crossed, undefined_chance / 100)
else: else:
if world.owKeepSimilar[player] and world.owParallel[player]: if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel':
for exitname, destname in parallelsimilar_connections: for exitname, destname in parallelsimilar_connections:
connect_two_way(world, exitname, destname, player, connected_edges) connect_two_way(world, exitname, destname, player, connected_edges)
@@ -584,7 +570,7 @@ def link_overworld(world, player):
remove_connected(forward_edge_sets, back_edge_sets) remove_connected(forward_edge_sets, back_edge_sets)
assert len(connected_edges) == len(default_connections) * 2, connected_edges assert len(connected_edges) == len(default_connections) * 2, connected_edges
valid_layout = world.accessibility[player] == 'none' or validate_layout(world, player) valid_layout = validate_layout(world, player)
tries -= 1 tries -= 1
assert valid_layout, 'Could not find a valid OW layout' assert valid_layout, 'Could not find a valid OW layout'
@@ -593,8 +579,166 @@ def link_overworld(world, player):
# flute shuffle # flute shuffle
logging.getLogger('').debug('Shuffling flute spots') logging.getLogger('').debug('Shuffling flute spots')
shuffle_flute_spots(world, player) def connect_flutes(flute_destinations):
for o in range(0, len(flute_destinations)):
owid = flute_destinations[o]
regions = flute_data[owid][0]
if not world.is_tile_swapped(owid, player):
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[0], player)
else:
connect_simple(world, 'Flute Spot ' + str(o + 1), regions[1], player)
if world.owFluteShuffle[player] == 'vanilla':
connect_flutes(default_flute_connections)
else:
flute_spots = 8
flute_pool = list(flute_data.keys())
new_spots = list()
ignored_regions = set()
used_flute_regions = []
forbidden_spots = []
forbidden_regions = []
def addSpot(owid, ignore_proximity, forced):
if world.owFluteShuffle[player] == 'balanced':
def getIgnored(regionname, base_owid, owid):
region = world.get_region(regionname, player)
for exit in region.exits:
if exit.connected_region is not None and exit.connected_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and exit.connected_region.name not in new_ignored:
if exit.connected_region.name in OWTileRegions and (OWTileRegions[exit.connected_region.name] in [base_owid, owid] or OWTileRegions[regionname] == base_owid):
new_ignored.add(exit.connected_region.name)
getIgnored(exit.connected_region.name, base_owid, OWTileRegions[exit.connected_region.name])
if regionname in one_way_ledges:
for ledge_region in one_way_ledges[regionname]:
if ledge_region not in new_ignored:
new_ignored.add(ledge_region)
getIgnored(ledge_region, base_owid, OWTileRegions[ledge_region])
if not world.is_tile_swapped(owid, player):
new_region = flute_data[owid][0][0]
else:
new_region = flute_data[owid][0][1]
if new_region in ignored_regions and not forced:
return False
new_ignored = {new_region}
getIgnored(new_region, OWTileRegions[new_region], OWTileRegions[new_region])
if not ignore_proximity and not forced and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions):
return False
ignored_regions.update(new_ignored)
if owid in flute_pool:
flute_pool.remove(owid)
if ignore_proximity and not forced:
logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}')
logging.getLogger('').debug(f'Placing flute at: {hex(owid)}')
new_spots.append(owid)
else:
# TODO: Inspect later, seems to happen only with 'random' flute shuffle
logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}')
return True
if world.customizer:
custom_spots = world.customizer.get_owflutespots()
if custom_spots and player in custom_spots:
if 'force' in custom_spots[player]:
for id in custom_spots[player]['force']:
owid = id & 0xBF
addSpot(owid, True, True)
flute_spots -= 1
if not world.is_tile_swapped(owid, player):
used_flute_regions.append(flute_data[owid][0][0])
else:
used_flute_regions.append(flute_data[owid][0][1])
if 'forbid' in custom_spots[player]:
for id in custom_spots[player]['forbid']:
owid = id & 0xBF
if owid not in new_spots:
forbidden_spots.append(owid)
if not world.is_tile_swapped(owid, player):
forbidden_regions.append(flute_data[owid][0][0])
else:
forbidden_regions.append(flute_data[owid][0][1])
# determine sectors (isolated groups of regions) to place flute spots
flute_regions = {(f[0][0] if (o not in world.owswaps[player][0]) != (world.mode[player] == 'inverted') else f[0][1]) : o for o, f in flute_data.items() if o not in new_spots and o not in forbidden_spots}
flute_sectors = [(len([r for l in s for r in l]), [r for l in s for r in l if r in flute_regions]) for s in world.owsectors[player]]
flute_sectors = [s for s in flute_sectors if len(s[1]) > 0]
region_total = sum([c for c,_ in flute_sectors])
sector_total = len(flute_sectors)
empty_sector_total = 0
sector_has_spot = []
# determine which sectors still need a flute spot
for sector in flute_sectors:
already_has_spot = any(region in sector for region in used_flute_regions)
sector_has_spot.append(already_has_spot)
if not already_has_spot:
empty_sector_total += 1
if flute_spots < empty_sector_total:
logging.getLogger('').warning(f'Warning: Not every sector can have a flute spot, generation might fail')
# pretend like some of the empty sectors already have a flute spot, don't know if they will be reachable
for i in range(len(flute_sectors)):
if not sector_has_spot[i]:
sector_has_spot[i] = True
empty_sector_total -= 1
if flute_spots == empty_sector_total:
break
# distribute flute spots for each sector
for i in range(len(flute_sectors)):
sector = flute_sectors[i]
sector_total -= 1
if not sector_has_spot[i]:
empty_sector_total -= 1
spots_to_place = min(flute_spots - empty_sector_total, max(0 if sector_has_spot[i] else 1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5)))
target_spots = len(new_spots) + spots_to_place
logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)')
if 0x30 in flute_pool and 0x30 not in forbidden_spots and len(new_spots) < target_spots and ('Desert Teleporter Ledge' in sector[1] or 'Mire Teleporter Ledge' in sector[1]):
addSpot(0x30, True, True) # guarantee desert/mire access
random.shuffle(sector[1])
f = 0
t = 0
while len(new_spots) < target_spots:
if f >= len(sector[1]):
f = 0
t += 1
if t > 5:
raise GenerationException('Infinite loop detected in flute shuffle')
owid = flute_regions[sector[1][f]]
if owid not in new_spots and owid not in forbidden_spots:
addSpot(owid, t > 0, False)
f += 1
region_total -= sector[0]
flute_spots -= spots_to_place
# connect new flute spots
new_spots.sort()
world.owflutespots[player] = new_spots
connect_flutes(new_spots)
# update spoiler
s = list(map(lambda x: ' ' if x not in new_spots else 'F', [i for i in range(0x40)]))
text_output = flute_spoiler_table.replace('s', '%s') % ( s[0x02], s[0x07],
s[0x00], s[0x03], s[0x05],
s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f],
s[0x0a], s[0x0f],
s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17],
s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e],
s[0x22], s[0x25], s[0x1a], s[0x1d],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e],
s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25],
s[0x3a],s[0x3b],s[0x3c], s[0x3f],
s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f],
s[0x32],s[0x33],s[0x34], s[0x37],
s[0x30], s[0x35],
s[0x3a],s[0x3b],s[0x3c], s[0x3f])
world.spoiler.set_map('flute', text_output, new_spots, player)
create_dynamic_flute_exits(world, player)
def connect_custom(world, connected_edges, groups, forced, player): def connect_custom(world, connected_edges, groups, forced, player):
forced_crossed, forced_noncrossed = forced forced_crossed, forced_noncrossed = forced
@@ -691,7 +835,7 @@ def connect_custom(world, connected_edges, groups, forced, player):
remove_pair_from_pool(edge1.name, edge2.name, is_crossed) remove_pair_from_pool(edge1.name, edge2.name, is_crossed)
connect_two_way(world, edge1.name, edge2.name, player, connected_edges) connect_two_way(world, edge1.name, edge2.name, player, connected_edges)
# resolve parallel # resolve parallel
if world.owParallel[player] and edge1.name in parallel_links_new: if world.owShuffle[player] == 'parallel' and edge1.name in parallel_links_new:
parallel_forward_edge = parallel_links_new[edge1.name] parallel_forward_edge = parallel_links_new[edge1.name]
parallel_back_edge = parallel_links_new[edge2.name] parallel_back_edge = parallel_links_new[edge2.name]
if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
@@ -707,13 +851,13 @@ def connect_custom(world, connected_edges, groups, forced, player):
connect_two_way(world, forward_edge, back_edge, player, connected_edges) connect_two_way(world, forward_edge, back_edge, player, connected_edges)
else: else:
raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
if world.owParallel[player] and forward_edge in parallel_links_new: if world.owShuffle[player] == 'parallel' and forward_edge in parallel_links_new:
parallel_forward_edge = parallel_links_new[forward_edge] parallel_forward_edge = parallel_links_new[forward_edge]
parallel_back_edge = parallel_links_new[back_edge] parallel_back_edge = parallel_links_new[back_edge]
if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed): if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge) raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, set_spoiler=True): def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
edge1 = world.get_entrance(edgename1, player) edge1 = world.get_entrance(edgename1, player)
edge2 = world.get_entrance(edgename2, player) edge2 = world.get_entrance(edgename2, player)
x = world.get_owedge(edgename1, player) x = world.get_owedge(edgename1, player)
@@ -737,7 +881,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, s
x.dest = y x.dest = y
y.dest = x y.dest = x
if set_spoiler and (world.owLayout[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none'): if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none':
world.spoiler.set_overworld(edgename2, edgename1, 'both', player) world.spoiler.set_overworld(edgename2, edgename1, 'both', player)
if connected_edges is not None: if connected_edges is not None:
@@ -745,7 +889,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, s
connected_edges.append(edgename2) connected_edges.append(edgename2)
# connecting parallel connections # connecting parallel connections
if world.owLayout[player] == 'vanilla' or world.owParallel[player]: if world.owShuffle[player] in ['vanilla', 'parallel']:
if edgename1 in parallel_links_new: if edgename1 in parallel_links_new:
try: try:
parallel_forward_edge = parallel_links_new[edgename1] parallel_forward_edge = parallel_links_new[edgename1]
@@ -834,7 +978,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player):
for whirl1, whirl2 in custom_whirlpools.items(): for whirl1, whirl2 in custom_whirlpools.items():
if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]): if [whirlpool_map[whirl1], whirlpool_map[whirl2]] not in merged_owids and should_merge_group(whirlpool_map[whirl1], whirlpool_map[whirl2]):
merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]]) merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]])
if world.owLayout[player] != 'vanilla': if world.owShuffle[player] != 'vanilla':
custom_edges = world.customizer.get_owedges() custom_edges = world.customizer.get_owedges()
if custom_edges and player in custom_edges: if custom_edges and player in custom_edges:
custom_edges = custom_edges[player] custom_edges = custom_edges[player]
@@ -940,9 +1084,6 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player):
exist_dw_regions.extend(dw_regions) exist_dw_regions.extend(dw_regions)
parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)] parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)]
if world.owLayout[player] == 'grid':
parity[1] = 0
parity[2] = 0
if not world.owKeepSimilar[player]: if not world.owKeepSimilar[player]:
parity[1] += 2*parity[2] parity[1] += 2*parity[2]
parity[2] = 0 parity[2] = 0
@@ -1036,16 +1177,12 @@ def define_tile_groups(world, do_grouped, player):
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']:
merge_groups([[0x05, 0x07]]) merge_groups([[0x05, 0x07]])
# special screens # all non-parallel screens
if world.owLayout[player] != 'wild' and (world.owCrossed[player] == 'none' or do_grouped): if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81]]) merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]])
# remaining non-parallel edges
if world.owLayout[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]])
# special case: non-parallel keep similar # special case: non-parallel keep similar
if world.owLayout[player] == 'wild' and world.owParallel[player] and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x28, 0x29]]) merge_groups([[0x28, 0x29]])
# whirlpool screens # whirlpool screens
@@ -1101,7 +1238,7 @@ def reorganize_groups(world, groups, player):
new_group[0] = None new_group[0] = None
if world.owTerrain[player]: if world.owTerrain[player]:
new_group[3] = None new_group[3] = None
if not world.owParallel[player]: if world.owShuffle[player] != 'parallel':
new_group[4] = None new_group[4] = None
if not world.owKeepSimilar[player]: if not world.owKeepSimilar[player]:
new_group[5] = None new_group[5] = None
@@ -1170,6 +1307,19 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player):
groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches) groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches)
return groups return groups
def create_dynamic_flute_exits(world, player):
flute_in_pool = True if player not in world.customitemarray else any(i for i, n in world.customitemarray[player].items() if i == 'flute' and n > 0)
if not flute_in_pool:
return
for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']):
if region.type == (RegionType.LightWorld if world.mode[player] != 'inverted' else RegionType.DarkWorld):
exitname = 'Flute From ' + region.name
exit = Entrance(region.player, exitname, region)
exit.spot_type = 'Flute'
exit.connect(world.get_region('Flute Sky', player))
region.exits.append(exit)
world.initialize_regions()
def get_mirror_exit_name(from_region, to_region): def get_mirror_exit_name(from_region, to_region):
if from_region in mirror_connections and to_region in mirror_connections[from_region]: if from_region in mirror_connections and to_region in mirror_connections[from_region]:
if len(mirror_connections[from_region]) == 1: if len(mirror_connections[from_region]) == 1:
@@ -1386,6 +1536,9 @@ 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] == 'none':
return True
entrance_connectors = { entrance_connectors = {
'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'], 'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'],
'Kakariko Suburb Area': ['Maze Race Ledge'], 'Kakariko Suburb Area': ['Maze Race Ledge'],
@@ -1479,7 +1632,7 @@ def validate_layout(world, player):
while unreachable_count != len(unreachable_regions): while unreachable_count != len(unreachable_regions):
# find unreachable regions # find unreachable regions
unreachable_regions = {} unreachable_regions = {}
for region_name in list(OWTileRegions.keys()): for region_name in list(OWTileRegions.copy().keys()):
if region_name not in explored_regions and region_name not in isolated_regions: if region_name not in explored_regions and region_name not in isolated_regions:
region = world.get_region(region_name, player) region = world.get_region(region_name, player)
unreachable_regions[region_name] = region unreachable_regions[region_name] = region
@@ -1522,55 +1675,9 @@ def validate_layout(world, player):
if len(unreachable_regions): if len(unreachable_regions):
return False return False
return True return True
def get_separate_ow_areas(world, player):
"""
Returns a list of separated areas in the overworld layout.
It looks at the distinct connected components when only considering
OW edge and whirlpool connections (no entrances, portals, mirror, or flute).
Uses Union-Find to handle directed edges properly (treats them as undirected).
"""
parent = {}
def find(x):
if x not in parent:
parent[x] = x
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
def union(x, y):
root_x = find(x)
root_y = find(y)
if root_x != root_y:
parent[root_y] = root_x
all_regions = set(OWTileRegions.keys()) - set(isolated_regions)
considered_exit_spot_types = set(['OpenTerrain', 'OWTerrain', 'Ledge', 'OWEdge', 'Whirlpool'])
# Initialize all regions in Union-Find
for region_name in all_regions:
find(region_name)
# Build connections by examining all edges (treating directed as undirected)
for region_name in all_regions:
region = world.get_region(region_name, player)
for exit in region.exits:
if exit.spot_type in considered_exit_spot_types and exit.connected_region is not None and exit.connected_region.name in all_regions:
union(region_name, exit.connected_region.name)
# Group regions by their root
areas = {}
for region_name in all_regions:
root = find(region_name)
if root not in areas:
areas[root] = []
areas[root].append(region_name)
return list(areas.values())
test_connections = [ test_connections = [
#('Links House ES', 'Octoballoon WS'), #('Links House ES', 'Octoballoon WS'),
#('Links House NE', 'Lost Woods Pass SW') #('Links House NE', 'Lost Woods Pass SW')
@@ -1784,6 +1891,10 @@ default_whirlpool_connections = [
((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')), ((0x35, 'Lake Hylia Whirlpool', 'Lake Hylia Water'), (0x0f, 'Zora Whirlpool', 'Zora Waterfall Water')),
((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')), ((0x12, 'Kakariko Pond Whirlpool', 'Kakariko Pond Area'), (0x3f, 'Octoballoon Whirlpool', 'Octoballoon Water')),
((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water')) ((0x55, 'Qirn Jump Whirlpool', 'Qirn Jump Water'), (0x7f, 'Bomber Corner Whirlpool', 'Bomber Corner Water'))
]
default_flute_connections = [
0x03, 0x16, 0x18, 0x2c, 0x2f, 0x30, 0x3b, 0x3f
] ]
ow_connections = { ow_connections = {
@@ -2236,11 +2347,6 @@ parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'),
('Dig Game ES', 'Frog WS') ('Dig Game ES', 'Frog WS')
] ]
special_screen_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
('Stone Bridge WC', 'Hobo EC'),
('Zora Waterfall NE', 'Zoras Domain SW')
]
# non shuffled overworld # non shuffled overworld
default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'), default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
('Lost Woods SW', 'Lost Woods Pass NW'), ('Lost Woods SW', 'Lost Woods Pass NW'),
@@ -2451,65 +2557,49 @@ isolated_regions = [
'Pyramid Water' 'Pyramid Water'
] ]
ow_loc_prize_table = { flute_data = {
'Master Sword Pedestal': (0x06d, 0x070), #OWID LW Region DW Region Slot VRAM BG Y BG X Link Y Link X Cam Y Cam X Unk1 Unk2 IconY IconX AltY AltX AltVRAM AltBGY AltBGX AltCamY AltCamX AltUnk1 AltUnk2 AltIconY AltIconX
'Hobo': (0xb80, 0xb90), 0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290),
'Mushroom': (0x180, 0x140), 0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x0138, 0x0550),
'Lost Woods Hideout Tree': (0x200, 0x320), 0x03: (['West Death Mountain (Bottom)', 'West Dark Death Mountain (Top)'], 0x0b, 0x1600, 0x02ca, 0x060e, 0x0328, 0x0678, 0x0337, 0x0683, 0xfff6, 0xfff2, 0x035b, 0x0680, 0x0118, 0x0860, 0x05c0, 0x00b8, 0x07ec, 0x0127, 0x086b, 0xfff8, 0x0004, 0x0148, 0x0850),
'Ether Tablet': (0x728, 0x017), 0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x0388, 0x0da8),
'Spectacle Rock': (0x7F8, 0x128), 0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0160, 0x0f20),
'Old Man': (0x6A8, 0x3FF), 0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0280, 0x0488),
'Floating Island': (0xD38, 0x038), 0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x02d0, 0x0f38),
'Death Mountain Bonk Rocks': (0xD48, 0x107), 0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x0448, 0x0058),
'Mountain Pass Pull Tree': (0x4E0, 0x380), 0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x0588, 0x0318),
'Mountain Pass Southeast Tree': (0x5A0, 0x3FF), 0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x0578, 0x04f8),
'Lost Woods Pass West Tree': (0x080, 0x540), 0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x0508, 0x0758),
'Kakariko Portal Tree': (0x190, 0x540), 0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x0580, 0x0928, 0x0580, 0x0948),
'Fortune Bonk Rocks': (0x288, 0x558), 0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x04f8, 0x0b60),
'Bonk Rocks Tree': (0x680, 0x520), 0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x0598, 0x0ccf),
'Sanctuary Tree': (0x788, 0x594), 0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x04e0, 0x0f68),
'River Bend West Tree': (0xA38, 0x5FF), 0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x07c0, 0x0210, 0x07c8, 0x01f8),
'River Bend East Tree': (0xB88, 0x558), 0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0548),
'Bottle Merchant': (0x178, 0x7E8), 0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07d8, 0x07f8),
'Blinds Hideout Tree': (0x118, 0x6F7), 0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0720, 0x0a80),
'Kakariko Welcome Tree': (0x358, 0x988), 0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09c0, 0x0c80),
'Hyrule Castle Tree': (0x730, 0x780), 0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0908, 0x04d8),
'Wooden Bridge Tree': (0xA78, 0x6F0), 0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x08b8, 0x0b08),
'Eastern Palace Tree': (0xEC0, 0x968), 0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0b88, 0x00b8),
'Maze Race': (0x0C0, 0xB00), 0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0ae0, 0x02c0),
'Flute Spot': (0x480, 0xAC0), 0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b10, 0x04e8),
'Flute Boy East Tree': (0x540, 0xB80), 0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b30, 0x0770),
'Flute Boy South Tree': (0x540, 0xB80), 0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0b20, 0x08b8),
'Central Bonk Rocks Tree': (0x768, 0xB37), 0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bc4, 0x0ad0),
'Tree Line Tree 2': (0xCD8, 0xB00), 0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0a78, 0x0c58),
'Tree Line Tree 4': (0xCD8, 0xB00), 0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0b50, 0x0f30),
'Desert Ledge': (0x018, 0xE88), 0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0fb0, 0x0070),
'Bombos Tablet': (0x3E8, 0xEF8), 0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x0568),
'Flute Boy Approach North Tree': (0x588, 0xD07), 0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0c80, 0x0628),
'Flute Boy Approach South Tree': (0x588, 0xD07), 0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0d60, 0x08d8),
'Lake Hylia Island': (0xB98, 0xD88), #0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0d88, 0x0a88),
'Purple Chest': (0x588, 0xE96), 0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0f90, 0x0da4),
'Sunken Treasure': (0x6F8, 0xF48), 0x37: (['Ice Cave Area', 'Shopping Mall Area'], 0x37, 0x0786, 0x0cf6, 0x0e2e, 0x0d58, 0x0ea0, 0x0d63, 0x0eab, 0x000a, 0x0002, 0x0d48, 0x0ed0),
0x3a: (['Desert Pass Area', 'Swamp Nook Area'], 0x3a, 0x001a, 0x0e08, 0x04c6, 0x0e70, 0x0540, 0x0e7d, 0x054b, 0x0006, 0x000a, 0x0e70, 0x0540),
'Dark Lumberjack Tree': (0x438, 0x1F7), 0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0f30, 0x0770),
'Bumper Cave Ledge': (0x538, 0x277), 0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0f38, 0x0898),
'Catfish': (0xE80, 0x368), 0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0f80, 0x0ef0)
'Dark Fortune Bonk Rocks (Drop 1)': (0x288, 0x558),
'Dark Fortune Bonk Rocks (Drop 2)': (0x288, 0x558),
'Dark Graveyard West Bonk Rocks': (0x900, 0x580),
'Dark Graveyard North Bonk Rocks': (0x900, 0x580),
'Dark Graveyard Tomb Bonk Rocks': (0x900, 0x580),
'Qirn Jump West Tree': (0xA98, 0x5FF),
'Qirn Jump East Tree': (0xB88, 0x558),
'Dark Witch Tree': (0xC28, 0x558),
'Pyramid': (0x998, 0x778),
'Pyramid Tree': (0x738, 0x908),
'Palace of Darkness Tree': (0xEC0, 0x968),
'Digging Game': (0x0C0, 0xB00),
'Dark Tree Line Tree 2': (0xCD8, 0xB00),
'Dark Tree Line Tree 3': (0xCD8, 0xB00),
'Dark Tree Line Tree 4': (0xCD8, 0xB00),
'Hype Cave Statue': (0x900, 0xDB0)
} }
tile_swap_spoiler_table = \ tile_swap_spoiler_table = \
@@ -2531,3 +2621,23 @@ H(38)| sss s| +-+-+-+-+-+-+-+-+
+-+ | s +-+-+-+ s +-+ +-+ | s +-+-+-+ s +-+
Zora: |s| H(38)| |s|s|s| |s| Zora: |s| H(38)| |s|s|s| |s|
+-+ +---+-+-+-+---+-+""" +-+ +---+-+-+-+---+-+"""
flute_spoiler_table = \
""" 0 1 2 3 4 5 6 7
+---+-+---+---+-+
01234567 A(00)| |s| | |s|
+--------+ | s +-+ s | s +-+
A(00)|s ss s s| B(08)| |s| | |s|
B(08)| s s| +-+-+-+-+-+-+-+-+
C(10)|ssssssss| C(10)|s|s|s|s|s|s|s|s|
D(18)|s ss ss | +-+-+-+-+-+-+-+-+
E(20)| s s | D(18)| |s| |s| |
F(28)|ssssssss| | s +-+ s +-+ s |
G(30)|s ssss s| E(20)| |s| |s| |
H(38)| sss s| +-+-+-+-+-+-+-+-+
+--------+ F(28)|s|s|s|s|s|s|s|s|
+-+-+-+-+-+-+-+-+
G(30)| |s|s|s| |s|
| s +-+-+-+ s +-+
H(38)| |s|s|s| |s|
+---+-+-+-+---+-+"""
+1 -33
View File
@@ -1,42 +1,10 @@
# Past Feature Notes # Past Feature Notes
1.5.0 1.4.3: File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!)
* Enemy Drop: Added "spies" and shadows for hidden enemies when enemy drop shuffled is enabled
* Pottery: Pots will uncolor when the item inside is collected next time the room is loaded
1.4.3
* File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!)
# Patch Notes # Patch Notes
Changelog archive Changelog archive
* 1.5.5
* Logic: Fixed an issue where PoD Bridge lead to Arena Main instead of Arena Landing Area. (Potentially unnecessarily requiring bombs or Somaria to progress)
* HUD: Key counters are correct even when door shuffle is off
* 1.5.4
* Documentation: New AI-assisted documentation [Site](https://aerinon.github.io/ALttPDoorRandomizer)
* Generation Error: Fixed Issue with Shop Code and Take Any Caves (thanks Codemann for assistance)
* 1.5.3
* Logic: Key logic fix for part of a dungeon located at Skull 3 (or other similar restricted entrancs). Appropriate key logic was not being applied, causing progression issues. This mostly affect crosskey style seeds.
* Standard: Rupee balancing algorithm can no longer switch out the weapon on uncle for money.
* Dungeon Counters: Some fixes for inconsistent tracking and display of dungeon counters on the keysanity menu.
* Text: Updated main tournament winner (Thanks clearmouse!)
* 1.5.1
* Bugfix: Fixed an issue with keys not counting correctly
* 1.5.0
* Logic: Fixed vanilla key logic for GT basement
* Logic (Playthrough): Fixed an issue where enemy kill rules were not applied during playthrough calculation. (Thanks Catobat for the catch)
* Keysanity/Keydrop Menu for DR:
* Map key information is now controlled by the Dungeon Chest Counts setting. If set to always on, this information will be available right away in the menu and will be on the HUD even when the map is not obtained.
* The key counter on the HUD for the current dungeon now accounts for keys from enemies or pots that are from vanilla key locations.
* The first number on the HUD represents all keys collected either in that dungeon or elsewhere.
* The second number on the HUD is the total keys that can be collected either in that dungeon or elsewhere.
* The key counter on inside the Menu is unchanged. (At the bottom near A button items)
* The first number in the Menu is the current number of keys in your inventory
* The second number is how many keys left to find in chests (not those from pots/enemies unless those item pools are enabled)
* Customizer: free_lamp_cone option added. The logic will account for this, and place the lamp without regard to dark rooms.
* Customizer: force_enemy option added that makes all enemies the specified type if possible. There are known gfx glitches in the overworld.
* Optimization: Improved generation performance (Thanks Catobat!)
* 1.4.11 * 1.4.11
* Rom fixes (all thanks to Codemann, I believe) * Rom fixes (all thanks to Codemann, I believe)
* Pot bug when at sprite limit * Pot bug when at sprite limit
+3 -3
View File
@@ -35,7 +35,7 @@ def main(args):
start_time = time.process_time() start_time = time.process_time()
# initialize the world # initialize the world
world = World(1, 'vanilla', True, 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False) world = World(1, 'vanilla', 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
world.player_names[1].append("Player 1") world.player_names[1].append("Player 1")
logger = logging.getLogger('') logger = logging.getLogger('')
@@ -168,9 +168,9 @@ def prefill_world(world, plando, text_patches):
elif line.startswith('!goal'): elif line.startswith('!goal'):
_, goalstr = line.split(':', 1) _, goalstr = line.split(':', 1)
world.goal = {1: goalstr.strip()} world.goal = {1: goalstr.strip()}
elif line.startswith('!owLayout'): elif line.startswith('!owShuffle'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.owLayout = {1: modestr.strip()} world.owShuffle = {1: modestr.strip()}
elif line.startswith('!owCrossed'): elif line.startswith('!owCrossed'):
_, modestr = line.split(':', 1) _, modestr = line.split(':', 1)
world.owCrossed = {1: modestr.strip()} world.owCrossed = {1: modestr.strip()}
+1 -1
View File
@@ -244,7 +244,7 @@ Ganons Tower - Validation Chest: Nothing
Ganon: Triforce Ganon: Triforce
# set Overworld connections (lines starting with $, separate edges with =) # set Overworld connections (lines starting with $, separate edges with =)
!owLayout: wild !owShuffle: parallel
#!owMixed: true # Mixed OW not supported yet #!owMixed: true # Mixed OW not supported yet
!owCrossed: none !owCrossed: none
!owKeepSimilar: true !owKeepSimilar: true
+6 -22
View File
@@ -132,24 +132,20 @@ Note: These changes do impact the logic. If you use `CodeTracker`, these Inverte
Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md) Only settings specifically added by this Overworld Shuffle fork are found here. All door and entrance randomizer settings are supported. See their [readme](https://github.com/Aerinon/ALttPDoorRandomizer/blob/master/README.md)
## Overworld Layout Shuffle (--ow_layout) ## Overworld Layout Shuffle (--ow_shuffle)
OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip) OW Edge Transitions are shuffled to create new world layouts. A brief visual representation of this can be viewed [here](https://zelda.codemann8.com/images/shared/ow-modes.gif). (This graphic also includes combinations of Crossed and Tile Flip)
### Vanilla ### Vanilla
OW Transitions are not shuffled. OW Transitions are not shuffled.
### Grid ### Parallel
OW Screens are shuffled in such a way that they are still arranged on an 8x8 grid for each world like vanilla, and the OW Transitions are based on that arrangement. OW Transitions are shuffled, but both worlds will have a matching layout, similar to that of vanilla.
### Wild ### Full
OW Transitions are shuffled with no respect to geometric coherence. OW Transitions are shuffled within each world separately.
## Parallel (--ow_unparallel to disable)
With OW Layout Shuffle, this forces both worlds to have a matching layout.
## Free Terrain (--ow_terrain) ## Free Terrain (--ow_terrain)
@@ -393,17 +389,11 @@ Districts are a concept originally conceived by Aerinon in the Door Randomizer,
Show the help message and exit. Show the help message and exit.
``` ```
--ow_layout <mode> --ow_shuffle <mode>
``` ```
For specifying the overworld layout shuffle you want as above. (default: vanilla) For specifying the overworld layout shuffle you want as above. (default: vanilla)
```
--ow_unparallel
```
With OW Layout Shuffle, this no longer forces both worlds to have a matching layout.
``` ```
--ow_terrain --ow_terrain
``` ```
@@ -434,12 +424,6 @@ This gives each OW tile a random chance to be flipped to the opposite world
For randomizing the flute spots around the overworld For randomizing the flute spots around the overworld
```
--ow_no_fog
```
With OW Grid Layout Shuffle or Mixed, this disables the fog that prevents you from seeing unvisited screens on the overworld map.
``` ```
--shuffle_followers --shuffle_followers
``` ```
+16 -7
View File
@@ -141,10 +141,19 @@ These are now independent of retro mode and have three options: None, Random, an
# Patch Notes # Patch Notes
* 1.5.6 * 1.5.0
* Enemy Drops: Pikit are no longer eligible for dropped items due to a vanilla bug where a failed steal can overwrite the assigned drop. * Logic: Fixed vanilla key logic for GT basement
* Standard Mode: Reworked spawn refills to be more generous for enemy drop modes. * Logic (Playthrough): Fixed an issue where enemy kill rules were not applied during playthrough calculation. (Thanks Catobat for the catch)
* Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely. * Enemy Drop: Added "spies" and shadows for hidden enemies when enemy drop shuffled is enabled
* Multiworld: Fixed a generation crash when beemizer is active and pottery is enabled. Pot locations are now properly filled with same-player items when the MW limit is hit even when beemizer has replaced native pot items. * Pottery: Pots will uncolor when the item inside is collected next time the room is loaded
* Keysanity/Keydrop Menu for DR:
* Map key information is now controlled by the Dungeon Chest Counts setting. If set to always on, this information will be available right away in the menu and will be on the HUD even when the map is not obtained.
* The key counter on the HUD for the current dungeon now accounts for keys from enemies or pots that are from vanilla key locations.
* The first number on the HUD represents all keys collected either in that dungeon or elsewhere.
* The second number on the HUD is the total keys that can be collected either in that dungeon or elsewhere.
* The key counter on inside the Menu is unchanged. (At the bottom near A button items)
* The first number in the Menu is the current number of keys in your inventory
* The second number is how many keys left to find in chests (not those from pots/enemies unless those item pools are enabled)
* Customizer: free_lamp_cone option added. The logic will account for this, and place the lamp without regard to dark rooms.
* Customizer: force_enemy option added that makes all enemies the specified type if possible. There are known gfx glitches in the overworld.
* Optimization: Improved generation performance (Thanks Catobat!)
+156 -314
View File
@@ -47,7 +47,6 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings
from source.item.FillUtil import valid_pot_items from source.item.FillUtil import valid_pot_items
from source.overworld.EntranceData import door_addresses, ow_prize_table from source.overworld.EntranceData import door_addresses, ow_prize_table
from source.overworld.EntranceShuffle2 import exit_ids from source.overworld.EntranceShuffle2 import exit_ids
from source.overworld.FluteShuffle import default_flute_connections, flute_data
from Text import ( from Text import (
Blacksmiths_texts, Blacksmiths_texts,
Blind_texts, Blind_texts,
@@ -85,7 +84,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc
from Versions import DRVersion, GKVersion, ORVersion from Versions import DRVersion, GKVersion, ORVersion
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'd80446af9eeb1726d2b6c1303bec1226' RANDOMIZERBASEHASH = '2647cc28bca3675152576dd1f5ea0bab'
class JsonRom(object): class JsonRom(object):
@@ -531,16 +530,14 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# patch flute spots # patch flute spots
owFlags = 0 owFlags = 0
owFog = 0 if world.owFluteShuffle[player] == 'vanilla':
if world.owFluteShuffle[player] == 'vanilla' and world.owLayout[player] != 'grid':
flute_spots = default_flute_connections flute_spots = default_flute_connections
else: else:
flute_spots = world.owflutespots[player] flute_spots = world.owflutespots[player]
owFlags |= 0x0100 owFlags |= 0x0100
if world.owFluteShuffle[player] != 'vanilla': write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA)
write_int16(rom, snes_to_pc(0x0AB80B), 0xEAEA)
flute_writes = [(f, flute_data[f][1]) for f in flute_spots] flute_writes = sorted([(f, flute_data[f][1]) for f in flute_spots], key = lambda f: f[1])
for o in range(0, len(flute_writes)): for o in range(0, len(flute_writes)):
owid = flute_writes[o][0] owid = flute_writes[o][0]
offset = 0 offset = 0
@@ -553,64 +550,46 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
write_int16(rom, snes_to_pc(0x02E8D1 + (o * 2)), data[13] if offset > 0 and len(data) > 13 else data[5]) # link Y write_int16(rom, snes_to_pc(0x02E8D1 + (o * 2)), data[13] if offset > 0 and len(data) > 13 else data[5]) # link Y
write_int16(rom, snes_to_pc(0x02E8F3 + (o * 2)), data[14] if offset > 0 and len(data) > 13 else data[6]) # link X write_int16(rom, snes_to_pc(0x02E8F3 + (o * 2)), data[14] if offset > 0 and len(data) > 13 else data[6]) # link X
base_index = 0 if offset == 0 or len(data) <= 15:
if not (offset == 0 or len(data) <= 15): write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[2]) # vram
base_index = 13 # use alternate flute data write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[3]) # BG scroll Y
write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[base_index + 2]) # vram write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[4]) # BG scroll X
write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[base_index + 3]) # BG scroll Y write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[7]) # cam Y
write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[base_index + 4]) # BG scroll X write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[8]) # cam X
if base_index > 0: write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[9]) # unknown 1
base_index -= 2 write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[10]) # unknown 2
write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[base_index + 7]) # cam Y rom.write_byte(snes_to_pc(0x0AB783 + o), data[12] & 0xff) # flute menu blip - X low byte
write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[base_index + 8]) # cam X rom.write_byte(snes_to_pc(0x0AB78B + o), data[12] // 0x100) # flute menu blip - X high byte
write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[base_index + 9]) # unknown 1 rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # flute menu blip - Y low byte
write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[base_index + 10]) # unknown 2 rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # flute menu blip - Y high byte
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, data[base_index + 12], data[base_index + 11], world.mode[player] == 'inverted') else: # use alternate flute data
rom.write_byte(snes_to_pc(0x0AB783 + o), map_x & 0xff) # flute menu blip - X low byte write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[15]) # vram
rom.write_byte(snes_to_pc(0x0AB78B + o), map_x // 0x100) # flute menu blip - X high byte write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[16]) # BG scroll Y
rom.write_byte(snes_to_pc(0x0AB793 + o), map_y & 0xff) # flute menu blip - Y low byte write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[17]) # BG scroll X
rom.write_byte(snes_to_pc(0x0AB79B + o), map_y // 0x100) # flute menu blip - Y high byte write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[18]) # cam Y
write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[19]) # cam X
write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[20]) # unknown 1
write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[21]) # unknown 2
rom.write_byte(snes_to_pc(0x0AB783 + o), data[23] & 0xff) # flute menu blip - X low byte
rom.write_byte(snes_to_pc(0x0AB78B + o), data[23] // 0x100) # flute menu blip - X high byte
rom.write_byte(snes_to_pc(0x0AB793 + o), data[22] & 0xff) # flute menu blip - Y low byte
rom.write_byte(snes_to_pc(0x0AB79B + o), data[22] // 0x100) # flute menu blip - Y high byte
# patch whirlpools # patch whirlpools
if world.owWhirlpoolShuffle[player]: if world.owWhirlpoolShuffle[player]:
owFlags |= 0x01 owFlags |= 0x01
write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player]) write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player])
# set custom overworld map layout and fog
if world.owLayout[player] == 'grid':
owFlags |= 0x06
owFog = 1 if world.owParallel[player] else 2
grid = world.owgrid[player]
all_rows = grid[0] + grid[1]
all_cells = sum(all_rows, [])
rom.write_bytes(0x153C80, all_cells)
for pos, cell_id in enumerate(sum(grid[0], [])):
rom.write_byte(0x153D00 + cell_id % 0x40, pos)
for pos, cell_id in enumerate(sum(grid[1], [])):
rom.write_byte(0x153D40 + cell_id % 0x40, pos)
elif world.owMixed[player]:
owFlags |= 0x02
owFog = 1
large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
for cell_id in range(0x80):
if cell_id - 0x01 in large_screen_ids:
screen_id = cell_id - 0x01
elif cell_id - 0x08 in large_screen_ids:
screen_id = cell_id - 0x08
elif cell_id - 0x09 in large_screen_ids:
screen_id = cell_id - 0x09
else:
screen_id = cell_id
world_flag = 0x40 if screen_id in world.owswaps[player][0] else 0x00
rom.write_byte(0x153C80 + cell_id, cell_id ^ world_flag)
# patch overworld edges # patch overworld edges
inverted_buffer = [0] * 0x82 inverted_buffer = [0] * 0x82
owMode = 0 owMode = 0
if world.owLayout[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]: if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]:
if world.owLayout[player] != 'vanilla': if world.owShuffle[player] == 'parallel':
owMode = 1 if world.owParallel[player] else 2 owMode = 1
if world.owKeepSimilar[player] and (world.owLayout[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'): elif world.owShuffle[player] == 'full':
owMode = 2
if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
owMode |= 0x0100 owMode |= 0x0100
if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]): if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]):
owMode |= 0x0200 owMode |= 0x0200
@@ -645,11 +624,10 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(0x1539B0 + b + 9, world_flag) rom.write_byte(0x1539B0 + b + 9, world_flag)
for edge in world.owedges: for edge in world.owedges:
if edge.player == player: if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player:
write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc) write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc)
if not edge.specialExit: if not edge.specialExit:
destination = edge.getTarget() if edge.dest is not None and isinstance(edge.dest, OWEdge) else 0xFF rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget())
rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, destination)
# patch bonk prizes # patch bonk prizes
if world.shuffle_bonk_drops[player]: if world.shuffle_bonk_drops[player]:
@@ -678,7 +656,6 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
write_int16(rom, 0x150002, owMode) write_int16(rom, 0x150002, owMode)
write_int16(rom, 0x150004, owFlags) write_int16(rom, 0x150004, owFlags)
write_int16(rom, 0x150008, owFog if world.owFog[player] else 0x00)
# patch entrance/exits/holes # patch entrance/exits/holes
for region in world.regions: for region in world.regions:
@@ -738,15 +715,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
if world.mirrorscroll[player] or world.doorShuffle[player] != 'vanilla': if world.mirrorscroll[player] or world.doorShuffle[player] != 'vanilla':
dr_flags |= DROptions.Town_Portal dr_flags |= DROptions.Town_Portal
if world.doorShuffle[player] == 'vanilla': if world.doorShuffle[player] == 'vanilla':
dr_flags |= DROptions.Eternal_Mini_Bosses dr_flags |= DROptions.Eternal_Mini_Bosses
if world.doorShuffle[player] not in ['vanilla', 'basic']: if world.doorShuffle[player] not in ['vanilla', 'basic']:
dr_flags |= DROptions.Map_Info dr_flags |= DROptions.Map_Info
if ((world.collection_rate[player] or world.goal[player] == 'completionist') if ((world.collection_rate[player] or world.goal[player] == 'completionist')
and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']): and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']):
dr_flags |= DROptions.Debug dr_flags |= DROptions.Debug
rom.write_byte(snes_to_pc(0x308039), 1) rom.write_byte(snes_to_pc(0x308039), 1)
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic' \ if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent': and world.mixed_travel[player] == 'prevent':
# PoD Falling Bridge or Hammjump # PoD Falling Bridge or Hammjump
# 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05
# 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01 # 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01
@@ -818,13 +795,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(0x138002, 2) rom.write_byte(0x138002, 2)
for name, layout in world.key_layout[player].items(): for name, layout in world.key_layout[player].items():
offset = compass_data[name][4]//2 offset = compass_data[name][4]//2
dungeon = world.get_dungeon(name, player)
if world.keyshuffle[player] == 'universal': if world.keyshuffle[player] == 'universal':
rom.write_byte(0x187010+offset, layout.max_chests + layout.max_drops) rom.write_byte(0x187010+offset, layout.max_chests + layout.max_drops)
else: else:
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops + dungeon.extra_small_keys) # not currently used rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used
rom.write_byte(0x187010+offset, layout.max_chests + dungeon.extra_small_keys) rom.write_byte(0x187010+offset, layout.max_chests)
rom.write_byte(0x187000+offset, dungeon.extra_small_keys)
builder = world.dungeon_layouts[player][name] builder = world.dungeon_layouts[player][name]
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
@@ -846,9 +821,9 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
if world.doorShuffle[player] == 'basic': if world.doorShuffle[player] == 'basic':
rom.write_byte(0x138002, 1) rom.write_byte(0x138002, 1)
for door in world.doors: for door in world.doors:
if door.dest is not None and isinstance(door.dest, Door) and \ if door.dest is not None and isinstance(door.dest, Door) and\
door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs, door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs,
DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]: DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
rom.write_bytes(door.getAddress(), door.dest.getTarget(door)) rom.write_bytes(door.getAddress(), door.dest.getTarget(door))
for paired_door in world.paired_doors[player]: for paired_door in world.paired_doors[player]:
rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player)) rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player))
@@ -948,7 +923,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(cr_pc+0x1e, 0xEE) # slash rom.write_byte(cr_pc+0x1e, 0xEE) # slash
rom.write_byte(cr_pc+0x1f, thousands_bot) rom.write_byte(cr_pc+0x1f, thousands_bot)
# modify stat config # modify stat config
stat_address = 0xA39895 stat_address = 0x23983E
stat_pc = snes_to_pc(stat_address) stat_pc = snes_to_pc(stat_address)
rom.write_byte(stat_pc, 0xa9) # change to pos 21 (from b1) 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+2, 0xc0) # change to 12 bits (from a0)
@@ -994,7 +969,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
set_inverted_mode(world, player, rom, inverted_buffer) set_inverted_mode(world, player, rom, inverted_buffer)
uncle_location = world.get_location('Link\'s Uncle', player) uncle_location = world.get_location('Link\'s Uncle', player)
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword', 'Sword and Shield']: if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword']:
# disable sword sprite from uncle # disable sword sprite from uncle
rom.write_bytes(0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
@@ -1007,10 +982,6 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_bytes(0x6D31B, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) rom.write_bytes(0x6D31B, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
bridge_item = world.get_location("Hobo", player).item
if bridge_item is None or not bridge_item.name.startswith("Bottle"):
rom.write_bytes(0x1E9C0, [0xFB, 0xFF, 0x03, 0x00, 0xAB, 0x00, 0x00, 0x00])
# set light cones # set light cones
if world.dark_rooms[player] == 'no_dark_rooms': if world.dark_rooms[player] == 'no_dark_rooms':
light_cone = 0x20 light_cone = 0x20
@@ -1262,7 +1233,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
gametype = 0x04 # item gametype = 0x04 # item
if (world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla' if (world.shuffle[player] != 'vanilla' or world.doorShuffle[player] != 'vanilla'
or world.dropshuffle[player] != 'none' or world.pottery[player] != 'none'): or world.dropshuffle[player] != 'none' or world.pottery[player] != 'none'):
gametype |= 0x02 # entrance/door gametype |= 0x02 # entrance/door
rom.write_byte(0x180211, gametype) # Game type rom.write_byte(0x180211, gametype) # Game type
@@ -1346,8 +1317,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
else: else:
goal_bytes += int16_as_bytes(req['target']) goal_bytes += int16_as_bytes(req['target'])
elif req['condition'] & 0x80 == 0: elif req['condition'] & 0x80 == 0:
if req['condition'] & 0x7F in [0x00, 0x06, 0x07]: if req['condition'] & 0x7F == 0x06 or req['condition'] & 0x7F == 0x07:
# no target value # agahnims have no target value
pass pass
elif req['condition'] & 0x7F < 0x08: elif req['condition'] & 0x7F < 0x08:
goal_bytes += [req['target']] goal_bytes += [req['target']]
@@ -1450,14 +1421,9 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
"bee": 0x10, "bee": 0x10,
"somaria": 0x11, "somaria": 0x11,
"byrna": 0x12, "byrna": 0x12,
"none": 0x7F, "none": 0xFF,
} }
ganon_item_value = ganon_item_byte[world.ganon_item[player]] rom.write_byte(0x18002E, ganon_item_byte[world.ganon_item[player]])
if world.require_ganon_item[player] and world.ganon_item[player] != "none":
if world.swords[player] != "swordless" or world.ganon_item[player] not in ["bombos", "ether", "quake"]:
ganon_item_value |= 0x80
rom.write_byte(0x18002E, ganon_item_value)
# block HC upstairs doors in rain state in standard mode # block HC upstairs doors in rain state in standard mode
prevent_rain = world.mode[player] == 'standard' and world.shuffle[player] != 'vanilla' and world.logic[player] != 'nologic' prevent_rain = world.mode[player] == 'standard' and world.shuffle[player] != 'vanilla' and world.logic[player] != 'nologic'
@@ -1493,53 +1459,31 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
| (0x04 if world.mapshuffle[player] != 'none' else 0x00) | (0x04 if world.mapshuffle[player] != 'none' else 0x00)
| (0x08 if world.bigkeyshuffle[player] != 'none' else 0x00))) # free roaming item text boxes | (0x08 if world.bigkeyshuffle[player] != 'none' else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] not in ['none', 'nearby'] else 0x00) # maps showing crystals on overworld rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] not in ['none', 'nearby'] else 0x00) # maps showing crystals on overworld
map_hud_mode = 0x00 if world.keyshuffle[player] != 'universal' and (world.mapshuffle[player] not in ['none', 'nearby'] or world.doorShuffle[player] != 'vanilla'
if world.dungeon_counters[player] == 'on': or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave']):
map_hud_mode = 0x02 # always on rom.write_byte(0x18003A, 0x01) # show key counts on map pickup
elif world.dungeon_counters[player] == 'off':
pass
elif world.keyshuffle[player] != 'universal' and (world.mapshuffle[player] not in ['none', 'nearby'] or world.doorShuffle[player] != 'vanilla'
or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave'] or world.dungeon_counters[player] == 'pickup'):
map_hud_mode = 0x01 # show on pickup
rom.write_byte(0x18003A, map_hud_mode)
loot_source = 0x09 loot_source = 0x09
if world.prizeshuffle[player] != 'none': if world.prizeshuffle[player] != 'none':
loot_source |= 0x10 loot_source |= 0x10
if world.pottery[player] != 'none': if world.pottery[player] not in ['none', 'cave']:
loot_source |= 0x02 loot_source |= 0x02
if world.dropshuffle[player] != 'none': if world.dropshuffle[player] != 'none':
loot_source |= 0x04 loot_source |= 0x04
rom.write_byte(0x1CFF20, loot_source) rom.write_byte(0x1CFF10, loot_source)
if world.loothud[player] == 'never':
rom.write_bytes(0x1CFF10, [0x00, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x00)
elif world.loothud[player] == 'presence':
rom.write_bytes(0x1CFF10, [0x01, 0x01, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
elif world.loothud[player] == 'value':
rom.write_bytes(0x1CFF10, [0x03, 0x03, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
elif world.loothud[player] == 'dungeon_value':
rom.write_bytes(0x1CFF10, [0x01, 0x03, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
elif world.loothud[player] == 'cave_value':
rom.write_bytes(0x1CFF10, [0x03, 0x01, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
if world.showloot[player] == 'never': if world.showloot[player] == 'never':
rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00]) rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF0F, 0x00) rom.write_byte(0x1CFF11, 0x00)
elif world.showloot[player] == 'presence': elif world.showloot[player] == 'presence':
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00]) rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF0F, 0x01) rom.write_byte(0x1CFF11, 0x00)
elif world.showloot[player] == 'compass': elif world.showloot[player] == 'compass':
rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x03, 0x00]) rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00])
rom.write_byte(0x1CFF0F, 0x01) rom.write_byte(0x1CFF11, 0x01)
elif world.showloot[player] == 'always': elif world.showloot[player] == 'always':
rom.write_bytes(0x1CFF08, [0x03, 0x00, 0x00, 0x00]) rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF0F, 0x01) rom.write_byte(0x1CFF11, 0x00)
if world.showmap[player] == 'visited': if world.showmap[player] == 'visited':
rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05]) rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05])
@@ -1553,7 +1497,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major
triforce_piece_ids = [0x6B, 0x6C] triforce_piece_ids = [0x6B, 0x6C]
if world.treasure_hunt_count[player] > 100: if world.treasure_hunt_count[player] > 20:
for triforce_piece_id in triforce_piece_ids: for triforce_piece_id in triforce_piece_ids:
rom.write_byte(loot_icons + triforce_piece_id, 0x04) rom.write_byte(loot_icons + triforce_piece_id, 0x04)
@@ -1591,53 +1535,36 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(0x18003C, compass_mode) rom.write_byte(0x18003C, compass_mode)
def get_entrance_coords(ent): def get_entrance_coords(ent):
if ent is None or isinstance(ent, int): if ent is None:
owid_map = [ 0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47 ] owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47]
coord_flags = 0x0000 x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0]
if ent is None: y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0]
# HUD-style dislocated icons world_indicator = 0x0000
x_map_position = [0x0050, 0x0080, 0xFF00, 0x0040, 0x0020, 0x00C0, 0x0060, 0x00A0, 0x00B0, 0x0080, 0x00E0] idx = int((map_index-2)/2)
y_map_position = [0x000C, 0x000C, 0xFF00, 0x00D4, 0x00D4, 0x00D4, 0x00D4, 0x00D4, 0x000C, 0x00D4, 0x00D4]
coord_flags = 0x4000 # raw OAM coord flag
idx = int((map_index-2)/2)
elif isinstance(ent, int):
# vanilla icon positions
x_map_position = [0x0F40, 0x0140, 0xFF00, 0x0778, 0x0F40, 0x0148, 0x00B0, 0x0CA0, 0x0900, 0x01F8, 0x0F20]
y_map_position = [0x0660, 0x0D00, 0xFF00, 0x0F50, 0x0660, 0x0D00, 0x00C0, 0x0E00, 0x0100, 0x0800, 0x0100]
idx = ent
owid = owid_map[idx] owid = owid_map[idx]
map_x = x_map_position[idx]
map_y = y_map_position[idx]
if owid != 0xFF: if owid != 0xFF:
if (owid < 0x40) == (owid in world.owswaps[player][0]): if (owid < 0x40) == (world.is_tile_swapped(owid, player)):
coord_flags |= 0x8000 # world indicator flag world_indicator = 0x8000
if coord_flags & 0x4000 == 0: return [world_indicator | x_map_position_generic[idx], y_map_position_generic[idx]]
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0) if type(ent) is Location:
return (coord_flags | map_x, map_y) from OverworldShuffle import OWTileRegions
elif type(ent) is Location: if ent.name == 'Hobo':
from OverworldShuffle import OWTileRegions, ow_loc_prize_table coords = (0xb80, 0xb80)
if ent.name in ow_loc_prize_table: elif ent.name == 'Master Sword Pedestal':
coords = ow_loc_prize_table[ent.name] coords = (0x06d, 0x070)
else: else:
owid = OWTileRegions[ent.parent_region.name] owid = OWTileRegions[ent.parent_region.name]
if owid == 0x81: if owid == 0x81:
coords = (0xF60, 0x280) coords = (0x220, 0xf40)
else: else:
owid = owid % 0x40 owid = owid % 0x40
coords = (0x200 * (owid % 0x08) + 0x100, 0x200 * int(owid / 0x08) + 0x100) coords = (0x200 * (owid % 0x08) + 0x100, 0x200 * int(owid / 0x08) + 0x100)
if owid in [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35]: if owid in [0x00, 0x03, 0x05, 0x18, 0x1b, 0x1e, 0x30, 0x35]:
coords = (coords[0] + 0x100, coords[1] + 0x100) coords = (coords[0] + 0x100, coords[1] + 0x100)
else: else:
if ent.name in ow_prize_table: coords = ow_prize_table[ent.name]
coords = ow_prize_table[ent.name] coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1])
elif door_addresses[ent.name][1] is not None:
coords = (door_addresses[ent.name][1][6], door_addresses[ent.name][1][5])
else:
raise Exception(f"No overworld map coordinates for entrance {ent.name}")
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, coords[0], coords[1], ent.parent_region.type == RegionType.DarkWorld)
coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | map_x, map_y)
return coords return coords
if world.overworld_map[player] == 'default': if world.overworld_map[player] == 'default':
# disable HC/AT/GT icons # disable HC/AT/GT icons
if not world.owMixed[player]: if not world.owMixed[player]:
@@ -1664,16 +1591,16 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
# prize location # prize location
write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords)
if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default': if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default':
swap_entrances = { 'Agahnims Tower': 'Ganons Tower', # TODO: I think this is logically the same as some of the vanilla stuff below
'Ganons Tower': 'Agahnims Tower' } vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)',
if dungeon in swap_entrances: 'Desert Palace': 'Desert Palace Entrance (North)',
entrance_name = dungeon 'Skull Woods': 'Skull Woods Final Section' }
if world.is_atgt_swapped(player): entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon
entrance_name = swap_entrances[dungeon] if world.is_atgt_swapped(player):
entrance = world.get_entrance(entrance_name, player) swap_entrances = { 'Agahnims Tower': 'Ganons Tower',
coords = get_entrance_coords(entrance) 'Ganons Tower': 'Agahnims Tower' }
else: entrance_name = swap_entrances[dungeon] if dungeon in swap_entrances else entrance_name
coords = get_entrance_coords(int((map_index-2)/2)) entrance = world.get_entrance(entrance_name, player)
else: else:
if len(portal_list) == 1: if len(portal_list) == 1:
portal_idx = 0 portal_idx = 0
@@ -1693,8 +1620,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
offset += 4 offset += 4
portal = world.get_portal(portal_list[portal_idx], player) portal = world.get_portal(portal_list[portal_idx], player)
entrance = portal.find_portal_entrance() entrance = portal.find_portal_entrance()
coords = get_entrance_coords(entrance) coords = get_entrance_coords(entrance)
# figure out compass entrances and what world (light/dark) # figure out compass entrances and what world (light/dark)
write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords)
if world.prizeshuffle[player] in ['none', 'dungeon', 'nearby'] and dungeon_table[dungeon].prize: if world.prizeshuffle[player] in ['none', 'dungeon', 'nearby'] and dungeon_table[dungeon].prize:
@@ -1791,40 +1718,53 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(0x180358, 0x01 if glitches_enabled else 0x00) rom.write_byte(0x180358, 0x01 if glitches_enabled else 0x00)
rom.write_byte(0x18008B, 0x01 if glitches_enabled else 0x00) rom.write_byte(0x18008B, 0x01 if glitches_enabled else 0x00)
if uncle_location.item is None or uncle_location.item.name not in ['Blue Shield', 'Red Shield', 'Mirror Shield', 'Progressive Shield', 'Sword and Shield']: # remove shield from uncle
# remove shield from uncle rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
write_int16(rom, 0x180183, 0) # Escape fill rupee bow
# Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [0] * 9)
rom.write_byte(0x18004E, 0) # Escape Fill (nothing)
write_int16(rom, 0x180183, 300) # Escape fill rupee bow
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
bow_max, bomb_max, magic_max = 0, 0, 0
bow_small, magic_small = 0, 0
if world.mode[player] == 'standard': if world.mode[player] == 'standard':
if world.doorShuffle[player] not in ['vanilla']:
# If door shuffle, give player small bit of all three ammos on respawn during escape
# Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [0x20, 3, 10] * 3)
# Always fully fill ammo of starting weapon on respawn during escape
if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']:
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
write_int16(rom, 0x180183, 300) # Escape fill rupee bow write_int16(rom, 0x180183, 300) # Escape fill rupee bow
rom.write_byte(0x180187, 70) # Uncle respawn refill arrows rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_byte(0x18018A, 70) # Zelda respawn refill arrows rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_byte(0x18018D, 70) # Mantle respawn refill arrowss rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows)
bow_max, bow_small = 70, 10
elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']: elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']:
rom.write_byte(0x180186, 50) # Uncle respawn refill bombs rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
rom.write_byte(0x180189, 50) # Zelda respawn refill bombs rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_byte(0x18018C, 50) # Mantle respawn refill bombs rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle respawn refills (magic, bombs, arrows)
bomb_max = 50
elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']:
rom.write_byte(0x180185, 0x80) # Uncle respawn refill magic rom.write_byte(0x18004E, 4) # Escape Fill (magic)
rom.write_byte(0x180188, 0x80) # Zelda respawn refill magic rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_byte(0x18018B, 0x80) # Mantle respawn refill magic rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
magic_max, magic_small = 0x80, 0x20
if world.doorShuffle[player] not in ['vanilla', 'basic']:
# Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
# Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
# Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest
rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)])
rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [magic_small, 3, bow_small]) # Mantle respawn refills (magic, bombs, arrows)
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved # patch swamp: Need to enable permanent drain of water as dam or swamp were moved
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00) rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00)
@@ -1927,7 +1867,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
write_enemy_shuffle_settings(world, player, rom) write_enemy_shuffle_settings(world, player, rom)
if (world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' if (world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none'
or world.pottery[player] != 'none'): 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:
if room.index in world.data_tables[player].room_list: if room.index in world.data_tables[player].room_list:
@@ -1944,7 +1884,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
write_enemizer_tweaks(rom, world, player) write_enemizer_tweaks(rom, world, player)
randomize_damage_table(rom, world, player) randomize_damage_table(rom, world, player)
write_strings(rom, world, player, team, is_mystery) write_strings(rom, world, player, team)
# write initial sram # write initial sram
rom.write_initial_sram() rom.write_initial_sram()
@@ -1989,7 +1929,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
(hashint >> 10) & 0x1F, (hashint >> 10) & 0x1F,
(hashint >> 5) & 0x1F, (hashint >> 5) & 0x1F,
hashint & 0x1F, hashint & 0x1F,
] ]
rom.write_bytes(0x180215, code) rom.write_bytes(0x180215, code)
rom.hash = code rom.hash = code
@@ -2026,15 +1966,7 @@ def write_custom_shops(rom, world, player):
if world.shopsanity[player] or shop.type == ShopType.TakeAny: if world.shopsanity[player] or shop.type == ShopType.TakeAny:
rom.write_byte(0x186E40 + shop.sram_address + index, 1) rom.write_byte(0x186E40 + shop.sram_address + index, 1)
if world.shopsanity[player] and shop.region.name in shop_to_location_table: if world.shopsanity[player] and shop.region.name in shop_to_location_table:
if shop.region.name == "Capacity Upgrade" and shop.item_count == 3: loc_item = world.get_location(shop_to_location_table[shop.region.name][index], player).item
if index == 1:
loc_item = ItemFactory(item['item'], player)
else:
if index == 2:
index = 1
loc_item = world.get_location(shop_to_location_table[shop.region.name][index], player).item
else:
loc_item = world.get_location(shop_to_location_table[shop.region.name][index], player).item
elif world.shopsanity[player] and shop.region.name in retro_shops: elif world.shopsanity[player] and shop.region.name in retro_shops:
loc_item = world.get_location(retro_shops[shop.region.name][index], player).item loc_item = world.get_location(retro_shops[shop.region.name][index], player).item
else: else:
@@ -2050,7 +1982,7 @@ def write_custom_shops(rom, world, player):
replace_price = int16_as_bytes(item['replacement_price']) replace_price = int16_as_bytes(item['replacement_price'])
item_max = item['max'] item_max = item['max']
item_player = 0 if item['player'] == player else item['player'] item_player = 0 if item['player'] == player else item['player']
item_data = [shop_id, item_id] + price + [item_max, replace] + replace_price + [item_player] item_data = [shop_id, item_id] + price + [item_max, replace] + replace_price + [item_player]
items_data.extend(item_data) items_data.extend(item_data)
rom.write_bytes(0x184800, shop_data) rom.write_bytes(0x184800, shop_data)
@@ -2095,49 +2027,6 @@ def hud_format_text(text):
output += b'\x7f\x00' output += b'\x7f\x00'
return output[:32] return output[:32]
def read_bytes(f, count):
values = f.read(count)
if len(values) < count:
raise EOFError
return values
def apply_rom_patches(rom, patches):
for patch in patches:
if not os.path.exists(f"patches/{patch}.ips"):
logging.getLogger('').warning("Patch %s not found -- skipping", patch)
continue
with open(f"patches/{patch}.ips", "rb") as f:
byte_changes = []
try:
header = read_bytes(f, 5)
if header != b"PATCH":
logging.getLogger('').warning("Patch %s invalid -- skipping", patch)
continue
while True:
address = read_bytes(f, 3)
if address == b"EOF":
break
address = int.from_bytes(address, byteorder="big")
length = int.from_bytes(read_bytes(f, 2), byteorder="big")
if length > 0:
byte_changes.append((address, list(f.read(length))))
else:
length = int.from_bytes(read_bytes(f, 2), byteorder="big")
value = read_bytes(f, 1)
byte_changes.append((address, length * list(value)))
for address, values in byte_changes:
rom.write_bytes(address, values)
logging.getLogger("").info("Patch %s applied successfully", patch)
except EOFError:
logging.getLogger('').warning("Patch %s invalid -- skipping", patch)
continue
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, triforce_gfx, def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, triforce_gfx,
ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx, ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx,
@@ -2454,7 +2343,7 @@ def write_string_to_rom(rom, target, string):
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes)) rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom, world, player, team, is_mystery=False): def write_strings(rom, world, player, team):
tt = TextTable() tt = TextTable()
tt.removeUnwantedText() tt.removeUnwantedText()
if world.shuffle[player] != 'vanilla': if world.shuffle[player] != 'vanilla':
@@ -2462,9 +2351,6 @@ def write_strings(rom, world, player, team, is_mystery=False):
" Crosskeys\n" " Crosskeys\n"
" Tournament\n" " Tournament\n"
" Winners\n{HARP}\n" " Winners\n{HARP}\n"
" ~~~2025~~~\n humbugh\n\n"
" ~~~2024~~~\n Gammachuu\n\n"
" ~~~2023~~~\n WallKicks\n\n"
" ~~~2022~~~\n Schulzer\n\n" " ~~~2022~~~\n Schulzer\n\n"
" ~~~2021~~~\n Goomba\n\n" " ~~~2021~~~\n Goomba\n\n"
" ~~~2020~~~\n Linlinlin\n\n" " ~~~2020~~~\n Linlinlin\n\n"
@@ -2505,15 +2391,6 @@ def write_strings(rom, world, player, team, is_mystery=False):
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances. # For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
if world.hints[player]: if world.hints[player]:
zoraitem = world.get_location('King Zora', player).item.hint_text
if len(zoraitem) <= 15:
tt['zora_meeting'] = f"Whaddaya want?\n ≥ {zoraitem.title()}\n Nothin'{{CHOICE}}"
else:
tt['zora_meeting'] = f"Do you want {zoraitem}?\n ≥ I'll pay\n No thanks{{CHOICE}}"
bottleitem = world.get_location('Bottle Merchant', player).item.hint_text
tt['bottle_vendor_choice'] = f"Do you want {bottleitem}?\n ≥ I'll take it\n No thanks!\n{{CHOICE}}"
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy() hint_locations = HintLocations.copy()
random.shuffle(hint_locations) random.shuffle(hint_locations)
@@ -2665,7 +2542,7 @@ def write_strings(rom, world, player, team, is_mystery=False):
if world.is_tile_swapped(0x18, player) or world.flute_mode[player] in ['active', 'pseudo']: if world.is_tile_swapped(0x18, player) or world.flute_mode[player] in ['active', 'pseudo']:
items_to_hint.remove(flute_item) items_to_hint.remove(flute_item)
flute_item = 'Ocarina (Activated)' flute_item = 'Ocarina (Activated)'
if world.owLayout[player] != 'vanilla' or world.owMixed[player]: if world.owShuffle[player] != 'vanilla' or world.owMixed[player]:
# Adding a guaranteed hint for the Flute in overworld shuffle. # Adding a guaranteed hint for the Flute in overworld shuffle.
this_location = world.find_items_not_key_only(flute_item, player) this_location = world.find_items_not_key_only(flute_item, player)
if this_location and this_location not in hinted_locations: if this_location and this_location not in hinted_locations:
@@ -2683,7 +2560,7 @@ def write_strings(rom, world, player, team, is_mystery=False):
random.shuffle(items_to_hint) random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
hint_count += 1 if world.owLayout[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
while hint_count > 0 and len(items_to_hint) > 0: while hint_count > 0 and len(items_to_hint) > 0:
this_item = items_to_hint.pop(0) this_item = items_to_hint.pop(0)
this_location = world.find_items_not_key_only(this_item, player) this_location = world.find_items_not_key_only(this_item, player)
@@ -2841,19 +2718,16 @@ def write_strings(rom, world, player, team, is_mystery=False):
loc.item = i loc.item = i
return loc return loc
(crystal5, crystal6, greenpendant) = tuple([x[0] if x else missing_prize() for x in [crystal5, crystal6, greenpendant]]) (crystal5, crystal6, greenpendant) = tuple([x[0] if x else missing_prize() for x in [crystal5, crystal6, greenpendant]])
bigbomb_follower = 'Big Bomb?\n'
if world.shuffle_followers[player]:
bigbomb_follower = ''
if world.prizeshuffle[player] in ['none', 'dungeon']: if world.prizeshuffle[player] in ['none', 'dungeon']:
(crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]]) (crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]])
tt['bomb_shop'] = f'{bigbomb_follower}My supply is blocked until you clear %s and %s.' % (crystal5, crystal6) tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6)
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant
elif world.prizeshuffle[player] == 'nearby': elif world.prizeshuffle[player] == 'nearby':
(crystal5, crystal6, greenpendant) = tuple([x.item.dungeon_object.name for x in [crystal5, crystal6, greenpendant]]) (crystal5, crystal6, greenpendant) = tuple([x.item.dungeon_object.name for x in [crystal5, crystal6, greenpendant]])
tt['bomb_shop'] = f'{bigbomb_follower}The crystals can be found near %s and %s.' % (crystal5, crystal6) tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found near %s and %s.' % (crystal5, crystal6)
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom near %s' % greenpendant tt['sahasrahla_bring_courage'] = 'I lost my family heirloom near %s' % greenpendant
else: else:
tt['bomb_shop'] = f'{bigbomb_follower}The crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text)
tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text
tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt[player] == 1 else 'You need %d crystals to enter.') % world.crystals_needed_for_gt[player] tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt[player] == 1 else 'You need %d crystals to enter.') % world.crystals_needed_for_gt[player]
@@ -2914,7 +2788,7 @@ def write_strings(rom, world, player, team, is_mystery=False):
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])
elif world.goal[player] == 'ganonhunt': elif world.goal[player] == 'ganonhunt':
tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon.' tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon'
elif world.goal[player] == 'bosshunt': elif world.goal[player] == 'bosshunt':
bosshunt_count = '%d guardian%s of %sdungeons' % \ bosshunt_count = '%d guardian%s of %sdungeons' % \
(world.bosses_ganon[player], (world.bosses_ganon[player],
@@ -2922,10 +2796,10 @@ def write_strings(rom, world, player, team, is_mystery=False):
'' if world.bosshunt_include_agas[player] else 'prize ') '' if world.bosshunt_include_agas[player] else 'prize ')
tt['sign_ganon'] = 'To beat Ganon you must defeat %s.' % bosshunt_count tt['sign_ganon'] = 'To beat Ganon you must defeat %s.' % bosshunt_count
elif world.goal[player] == 'completionist': elif world.goal[player] == 'completionist':
tt['sign_ganon'] = 'Ganon only respects those who have done everything.' 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'] = 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_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI cannot die!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
def get_custom_goal_text(type): def get_custom_goal_text(type):
goal_text = world.custom_goals[player][type]['goaltext'] goal_text = world.custom_goals[player][type]['goaltext']
@@ -2944,13 +2818,7 @@ def write_strings(rom, world, player, team, is_mystery=False):
if world.custom_goals[player]['murahgoal'] and 'goaltext' in world.custom_goals[player]['murahgoal']: if world.custom_goals[player]['murahgoal'] and 'goaltext' in world.custom_goals[player]['murahgoal']:
tt['murahdahla'] = get_custom_goal_text('murahgoal') tt['murahdahla'] = get_custom_goal_text('murahgoal')
tavern_texts = random.sample(TavernMan_texts, 5) tt['kakariko_tavern_fisherman'] = TavernMan_texts[random.randint(0, len(TavernMan_texts) - 1)]
tt['tavern_old_man_awake'] = tavern_texts.pop()
tt['tavern_old_man_unactivated_flute'] = tavern_texts.pop()
tt['tavern_old_man_know_tree_unactivated_flute'] = tavern_texts.pop()
tt['tavern_old_man_have_flute'] = tavern_texts.pop()
tt['kakariko_tavern_fisherman'] = tavern_texts.pop()
pedestalitem = world.get_location('Master Sword Pedestal', player).item pedestalitem = world.get_location('Master Sword Pedestal', player).item
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem, True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item' pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem, True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
@@ -2984,22 +2852,7 @@ def write_strings(rom, world, player, team, is_mystery=False):
sanc_text = "Dark Chapel" sanc_text = "Dark Chapel"
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n{CHOICE3}" tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n Mountain Cave\n{CHOICE2}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n Mountain Cave\n{CHOICE2}"
if world.mode[player] == 'inverted':
if is_mystery:
tt['intro_main'] = CompressedTextMapper.convert(
"{INTRO}\n Episode III"
+ "{PAUSE3}\n A Link to the Past"
+ "{PAUSE3}\n Mystery Randomizer"
+ "{PAUSE3}\n\n\n"
+ "{PAUSE3}\nAfter mostly disregarding what happened in the first two games,"
+ "{PAUSE3}\nLink awakens to a massive mystery."
+ "{PAUSE3}\nHe doesn't know why he's here, or what he must do."
+ "{PAUSE3} {CHANGEPIC}\nGanon has moved around all the items in Hyrule."
+ "{PAUSE7}\nYou will have to find all the items necessary to achieve your goal."
+ "{PAUSE7}\nThis is your chance to be a hero."
+ "{PAUSE3} {CHANGEPIC}\nGood luck out there, and try not to die."
+ "{PAUSE9} {CHANGEPIC}", False)
elif world.mode[player] == 'inverted':
tt['intro_main'] = CompressedTextMapper.convert( tt['intro_main'] = CompressedTextMapper.convert(
"{INTRO}\n Episode III" "{INTRO}\n Episode III"
+ "{PAUSE3}\n A Link to the Past" + "{PAUSE3}\n A Link to the Past"
@@ -3013,7 +2866,6 @@ def write_strings(rom, world, player, team, is_mystery=False):
+ "{PAUSE7}\nThis is your chance to be a hero." + "{PAUSE7}\nThis is your chance to be a hero."
+ "{PAUSE3} {CHANGEPIC}\nYou must get the 7 crystals to beat Ganon." + "{PAUSE3} {CHANGEPIC}\nYou must get the 7 crystals to beat Ganon."
+ "{PAUSE9} {CHANGEPIC}", False) + "{PAUSE9} {CHANGEPIC}", False)
rom.write_bytes(0xE0000, tt.getBytes()) rom.write_bytes(0xE0000, tt.getBytes())
credits = Credits() credits = Credits()
@@ -3342,13 +3194,6 @@ def update_compasses(rom, dungeon_locations, world, player):
if not provided_dungeon: if not provided_dungeon:
rom.write_byte(0x186FFF, 0xff) rom.write_byte(0x186FFF, 0xff)
def adjust_ow_coordinates_to_layout(world, player, x, y, dw_flag):
if world.owLayout[player] != 'grid':
return (x, y)
layout_map = world.owlayoutmap_dw[player] if dw_flag else world.owlayoutmap_lw[player]
original_slot_id = ((y // 0x0200) % 0x08) * 0x08 + ((x // 0x0200) % 0x08)
new_slot_id = layout_map[original_slot_id]
return ((new_slot_id % 0x08) * 0x0200 + x % 0x0200, ((new_slot_id // 0x08) % 0x08) * 0x0200 + y % 0x0200)
InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main', InconvenientDungeonEntrances = {'Turtle Rock': 'Turtle Rock Main',
@@ -3622,10 +3467,7 @@ Prizes = ['Green Pendant',
] ]
hash_alphabet = [ hash_alphabet = [
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake",
"Ice Rod", "Green Pendant", "Bombos", "Ether", "Quake", "Lamp", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
"Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
"Green Potion", "Somaria", "Cape", "Mirror", "Boots", "Gloves",
"Flippers", "Pearl", "Shield", "Green Tunic", "Heart", "Map",
"Compass", "Key",
] ]
+8 -10
View File
@@ -505,7 +505,7 @@ def global_rules(world, player):
set_rule(world.get_entrance('Skull Woods Rock (West)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Skull Woods Rock (West)', player), lambda state: state.can_lift_rocks(player))
set_rule(world.get_entrance('Skull Woods Rock (East)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Skull Woods Rock (East)', player), lambda state: state.can_lift_rocks(player))
# this more like an ohko rule - dependent on bird being present too - so enemizer could turn this off? # this more like an ohko rule - dependent on bird being present too - so enemizer could turn this off?
set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player)) set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: world.can_take_damage or state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player))
set_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), lambda state: state.can_lift_rocks(player))
set_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), lambda state: state.can_lift_rocks(player))
set_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player))
@@ -1026,11 +1026,10 @@ def global_rules(world, player):
world.get_location('Ganon', player), world.get_location('Ganon', player),
lambda state: state.has_beam_sword(player) lambda state: state.has_beam_sword(player)
and state.has_fire_source(player) and state.has_fire_source(player)
and (state.can_hit_stunned_ganon(player) or ( and (state.has('Tempered Sword', player) or state.has('Golden Sword', player)
not world.require_ganon_item[player] and ( or state.can_hit_stunned_ganon(player)
state.has('Tempered Sword', player) or state.has('Golden Sword', player) or state.has('Lamp', player)
or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
or state.can_extend_magic(player, 12))))) # need to light torch a sufficient amount of times
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop
@@ -1405,7 +1404,8 @@ def ow_bunny_rules(world, player):
add_bunny_rule(world.get_entrance('Skull Woods Forgotten Bush (East)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Forgotten Bush (East)', player), player)
add_bunny_rule(world.get_entrance('Skull Woods Second Section Hole', player), player) add_bunny_rule(world.get_entrance('Skull Woods Second Section Hole', player), player)
add_bunny_rule(world.get_entrance('East Dark Death Mountain Bushes', player), player) add_bunny_rule(world.get_entrance('East Dark Death Mountain Bushes', player), player)
add_bunny_rule(world.get_entrance('Bumper Cave Ledge Drop', player), player) if not world.can_take_damage:
add_bunny_rule(world.get_entrance('Bumper Cave Ledge Drop', player), player)
add_bunny_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), player) add_bunny_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), player)
add_bunny_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), player) add_bunny_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), player)
add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player) add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player)
@@ -1832,9 +1832,7 @@ def standard_rules(world, player):
add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player))
if not world.is_premature_copied_world: if not world.is_premature_copied_world:
loc = world.get_location_unsafe('Hyrule Castle Courtyard Tree Pull', player) add_rule(world.get_location('Hyrule Castle Courtyard Tree Pull', player), lambda state: state.has('Zelda Delivered', player))
if loc:
add_rule(loc, lambda state: state.has('Zelda Delivered', player))
# don't allow bombs to get past here before zelda is rescued # don't allow bombs to get past here before zelda is rescued
set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player))
+113 -111
View File
@@ -50,18 +50,18 @@ Uncle_texts = [
"Don't worry.\nI got this\ncovered.", "Don't worry.\nI got this\ncovered.",
"Race you to\nthe castle!", "Race you to\nthe castle!",
"\n Hi", "\n Hi",
"I'm just going\nout for a\npack of smokes.", "I'M JUST GOING\nOUT FOR A\nPACK OF SMOKES",
"It's dangerous\nto go alone.\nSee ya!", "It's dangerous\nto go alone.\nSee ya!",
"Are you a bad\nenough dude to\nrescue Zelda?", "ARE YOU A BAD\nENOUGH DUDE TO\nRESCUE ZELDA?",
"\n\n I AM ERROR", "\n\n I AM ERROR",
"This seed is\nsub 2 hours,\nguaranteed.", "This seed is\nsub 2 hours,\nguaranteed.",
"The chest is\na secret to\neverybody.", "The chest is\na secret to\neverybody.",
"I'm off to\nfind the\nwind fish.", "I'm off to\nfind the\nwind fish.",
"The shortcut\nto Ganon\nis this way!", "The shortcut\nto Ganon\nis this way!",
"The moon is\ncrashing! Run\nfor your life!", "THE MOON IS\nCRASHING! RUN\nFOR YOUR LIFE!",
"Time to fight\nhe who must\nnot be named.", "Time to fight\nhe who must\nnot be named.",
"Red mail\nis for\ncowards.", "RED MAIL\nIS FOR\nCOWARDS.",
"Hey!\n\nListen!", "HEY!\n\nLISTEN!",
"Well\nexcuuuuuse me,\nprincess!", "Well\nexcuuuuuse me,\nprincess!",
"5,000 Rupee\nreward for >\nYou're boned.", "5,000 Rupee\nreward for >\nYou're boned.",
"Welcome to\nStoops Lonk's\nHoose", "Welcome to\nStoops Lonk's\nHoose",
@@ -135,7 +135,6 @@ Triforce_texts = [
" I promise the\n next seed will\n be better.", " I promise the\n next seed will\n be better.",
"\n Honk.", "\n Honk.",
" Breakfast\n is served!", " Breakfast\n is served!",
"\n send help",
] ]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!'] BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?'] Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']
@@ -230,92 +229,97 @@ Ganon1_texts = [
] ]
Ganon_Phase_3_No_Silvers_texts = [ Ganon_Phase_3_No_Silvers_texts = [
"Did you find\nthe arrows\non Planet Zebes?", "Did you find\nthe arrows on\nPlanet Zebes?",
"Did you find\nthe arrows?\nI think not.", "Did you find\nthe arrows?\nI think not.",
"Silver arrows?\nI have never\nheard of them.", "Silver arrows?\nI have never\nheard of them",
"Did you find\nthe arrows on\nThe Moon?", "Did you find\nthe arrows on\nThe Moon?",
"Did you find\nthe arrows\nin /dev/null?", "Did you find\nthe arrows\nIn dev null?",
"I have sold\nthe arrows for\na green big 20.", "I have sold\nthe arrows for\na green big 20",
"Did you find\nthe arrows in\nCount Dracula?", "Did you find\nthe arrows in\nCount Dracula?",
" Error 404\nSilver Arrows\n Not Found", "Error 404\nSilver arrows\nnot found.",
"No arrows\nfor you today.\nSorry.", "No arrows for\nyou today.\nSorry.",
"No arrows?\nCheck your\njunk mail." "No arrows?\nCheck your\njunk mail."
"Did you find\nthe arrows\nin Jabu's belly?", "Did you find\nthe arrows in\nJabu's belly?",
"Silver is not\nan appropriate\narrow material.", "Silver is not\nan appropriate\narrow material",
"Did you find\nthe arrows\nin nNarnia?", "Did you find\nthe arrows in\nNarnia?",
"Did you find\nthe arrows\nin nKefka's Tower.", "DID YOU FIND\nTHE ARROWS IN\nKEFKA'S TOWER",
"Did you find\nthe arrows\nin nyour Recycle Bin?", "Did you find\nthe arrows in\nyour Recycle Bin?",
"Silver Arrows?\n\nLUL", "Silver Arrows?\n\nLUL",
"\nImagine finding\nthe arrows.", "Imagine\nfinding the\narrows",
"Did you find\nthe arrows\nin scenic Ohio?", "Did you find\nsilvers in\nscenic Ohio?",
"Did you find\nthe arrows\nin •mumblemumble•", "Did you find\nthe arrows in\n*mumblemumble*",
"Did you find\nthe arrows\nin the hourglass?", "Did you find\nthe arrows in\nthe hourglass?",
"\nSilver Arrows\nare so main branch.", "Silver Arrows\nare so v30",
"Did you find\nthe arrows\nin World 4-2?", "OH, NO, THEY\nACTUALLY SAID\nSILVER MARROW",
"Did you find\nthe arrows\nin Ghanti's ears?", "SURELY THE\nLEFTMOST TILES\nWILL STAY UP",
"Did you find\nthe arrows in\nWorld 4-2?",
"SILLY HERO,\nSILVER IS FOR\nWEREWOLVES!",
"Did you find\nthe silvers in\nGanti's ears",
] ]
Ganon_Phase_3_No_Weakness_texts = [ Ganon_Phase_3_No_Weakness_texts = [
"\n Error 404\nWeakness Not Found", "Error 404\nWeakness\nnot found.",
"No weakness\nfor you today.\nSorry.", "No weakness\nfor you today.\nSorry",
"Careful, all\nthat spinning\nmakes me dizzy.", "Careful, all\nthat spinning\nmakes me dizzy",
"Are you ready\nto spin\nto win?", "Are you ready\nTo spin\nTo win?",
"\n Spin To Win!\n", "\nSpin To Win!\n",
"Surely the\nleftmost tiles\nwill stay up!", "SURELY THE\nLEFTMOST TILES\nWILL STAY UP",
"You spin me\nright round\nlike a record.", "You Spin Me\nRight Round\nLike A Record",
"Oh, no, they\nactually said\nsilver •marrow•.", "SILLY HERO,\nSILVER IS FOR\nWEREWOLVES!",
"Silly hero,\nsilver is for\nwerewolves!",
] ]
TavernMan_texts = [ TavernMan_texts = [
"What do you call a blind dinosaur?\n\nA doyouthink-\nhesaurus.", "What do you\ncall a blind\ndinosaur?\na doyouthink-\nhesaurus.",
"What do ducks like to eat?\n\nQuackers!", "A blind man\nwalks into\na bar.\nAnd a table.\nAnd a chair.",
"How do you set up a party in space?\n\nYou planet!", "What do ducks\nlike to eat?\n\nQuackers!",
"I'm glad I know sign language.\nIt's pretty handy.", "How do you\nset up a party\nin space?\n\nYou planet!",
"What did Zelda say to Link at a secure door?\n\nTriforce!", "I'm glad I\nknow sign\nlanguage.\nIt's pretty\nhandy.",
"I am on a seafood diet.\n\nEvery time I see food, I eat it.", "What did Zelda\nsay to Link at\na secure door?\n\nTRIFORCE!",
"I've decided to sell my vacuum.\n\nIt was just gathering dust.", "I am on a\nseafood diet.\n\nEvery time\nI see food,\nI eat it.",
"What's the best time to go to the dentist?\n\nTooth-hurtie!", "I've decided\nto sell my\nvacuum.\nIt was just\ngathering\ndust.",
"Why can't a bike stand on its own?\n\nIt's two-tired!", "What's the best\ntime to go to\nthe dentist?\n\nTooth-hurtie!",
"If you haven't found Quake yet…\n\nit's not your fault.", "Why can't a\nbike stand on\nits own?\n\nIt's two-tired!",
"Why is Peter Pan always flying?\n\nBecause he Neverlands!", "If you haven't\nfound Quake\nyet…\nit's not your\nfault.",
"I once told a joke to Armos.\n\nBut he remained stone-faced!", "Why is Peter\nPan always\nflying?\nBecause he\nNeverlands!",
"Lanmola was late to our dinner party.\n\nHe just came for the desert", "I once told a\njoke to Armos.\n\nBut he\nremained\nstone-faced!",
"Moldorm is such a prankster.\n\nAnd I fall for it every time!", "Lanmola was\nlate to our\ndinner party.\nHe just came\nfor the desert",
"Helmasaur is throwing a party.\n\nI hope it's a masquerade!", "Moldorm is\nsuch a\nprankster.\nAnd I fall for\nit every time!",
"I'd like to know Arrghus better.\n\nBut he won't come out of his shell!", "Helmasaur is\nthrowing a\nparty.\nI hope it's\na masquerade!",
"Mothula didn't have much fun at the party.\n\nHe's immune to spiked punch!", "I'd like to\nknow Arrghus\nbetter.\nBut he won't\ncome out of\nhis shell!",
"Kholdstare is afraid to go to the circus.\n\nHungry kids thought he was cotton candy!", "Mothula didn't\nhave much fun\nat the party.\nHe's immune to\nspiked punch!",
"I asked who Vitreous' best friends are.\n\nHe said, \"Me, Myself, and Eye!\"", "Don't set me\nup with that\nchick from\nSteve's Town.\n\n\nI'm not\ninterested in\na Blind date!",
"Trinexx can be a hothead or he can be an ice guy. In the end, he's a solid individual!", "Kholdstare is\nafraid to go\nto the circus.\nHungry kids\nthought he was\ncotton candy!",
"Bari thought I had moved out of town.\n\nHe was shocked to see me!", "I asked who\nVitreous' best\nfriends are.\nHe said,\n'Me, Myself,\nand Eye!'",
"Don't argue with a frozen Deadrock.\n\nHe'll never change his position!", "Trinexx can be\na hothead or\nhe can be an\nice guy. In\nthe end, he's\na solid\nindividual!",
"I offered a drink to a self-loathing Ghini.\n\nHe said he didn't like spirits!", "Bari thought I\nhad moved out\nof town.\nHe was shocked\nto see me!",
"I was supposed to meet Gibdo for lunch.\n\nBut he got wrapped up in something!", "I can only get\nWeetabix\naround here.\nI have to go\nto Steve's\nTown for Count\nChocula!",
"Goriya sure has changed in this game.\n\nI hope he comes back around!", "Don't argue\nwith a frozen\nDeadrock.\nHe'll never\nchange his\nposition!",
"Hinox actually wants to be a lawyer.\n\nToo bad he bombed the bar exam!", "I offered a\ndrink to a\nself-loathing\nGhini.\nHe said he\ndidn't like\nspirits!",
"I'm surprised Moblin's tusks are so gross.\n\nHe always has his Trident with him!", "I was supposed\nto meet Gibdo\nfor lunch.\nBut he got\nwrapped up in\nsomething!",
"Don't tell Stalfos I'm here.\n\nHe has a bone to pick with me!", "Goriya sure\nhas changed\nin this game.\nI hope he\ncomes back\naround!",
"I got Wallmaster to help me move furniture.\n\nHe was really handy!", "Hinox actually\nwants to be a\nlawyer.\nToo bad he\nbombed the\nBar exam!",
"Wizzrobe was just here.\n\nHe always vanishes right before we get the check!", "I'm surprised\nMoblin's tusks\nare so gross.\nHe always has\nhis Trident\nwith him!",
"I shouldn't have picked up Zora's tab.\n\nThat guy drinks like a fish!", "Don't tell\nStalfos I'm\nhere.\nHe has a bone\nto pick with\nme!",
"I was sharing a drink with Poe.\n\nFor no reason, he left in a heartbeat!", "I got\nWallmaster to\nhelp me move\nfurniture.\nHe was really\nhandy!",
"Don't trust horsemen on Death Mountain.\n\nThey're Lynel the time!", "Wizzrobe was\njust here.\nHe always\nvanishes right\nbefore we get\nthe check!",
"Today's special is battered bat.\n\nGot slapped for offering a lady a Keese!", "I shouldn't\nhave picked up\nZora's tab.\nThat guy\ndrinks like\na fish!",
"Don't walk under propellered pineapples.\n\nYou may end up wearing a pee hat!", "I was sharing\na drink with\nPoe.\nFor no reason,\nhe left in a\nheartbeat!",
"My girlfriend burrowed under the sand.\n\nSo I decided to Leever!", "Don't trust\nhorsemen on\nDeath Mountain.\nThey're Lynel\nthe time!",
"Geldman wants to be a Broadway star.\n\nHe's always practicing Jazz Hands!", "Today's\nspecial is\nbattered bat.\nGot slapped\nfor offering a\nlady a Keese!",
"Octoballoon must be mad at me.\n\nHe blows up at the sight of me!", "Don't walk\nunder\npropellered\npineapples.\nYou may end up\nwearing\na pee hat!",
"Toppo is a total pothead.\n\nHe hates it when you take away his grass", "My girlfriend\nburrowed under\nthe sand.\nSo I decided\nto Leever!",
"I lost my shield by that house.\n\nWhy did they put up a Pikit fence?!", "Geldman wants\nto be a\nBroadway star.\nHe's always\npracticing\nJazz Hands!",
"Know that fox in Steve's Town?\n\nHe'll Pikku pockets if you aren't careful", "Octoballoon\nmust be mad\nat me.\nHe blows up\nat the sight\nof me!",
"Dash through Dark World bushes.\n\nYou'll see Ganon is tryin to Stal you!", "Toppo is a\ntotal pothead.\n\nHe hates it\nwhen you take\naway his grass",
"Eyegore!\n\nYou gore!\n\nWe all gore those jerks with arrows!", "I lost my\nshield by\nthat house.\nWhy did they\nput up a\nPikit fence?!",
"I like my whiskey neat.\n\nSome prefer it Octoroks!", "Know that fox\nin Steve's\nTown?\nHe'll Pikku\npockets if you\naren't careful",
"I consoled Freezor over a cup of coffee.\n\nHis problems just seemed to melt away!", "Dash through\nDark World\nbushes.\nYou'll see\nGanon is tryin\nto Stal you!",
"Magic droplets of water don't shut up.\n\nThey just Kyameron!", "Eyegore!\n\nYou gore!\nWe all gore\nthose jerks\nwith arrows!",
"I bought hot wings for Sluggula.\n\nThey gave him explosive diarrhea!", "I like my\nwhiskey neat.\n\nSome prefer it\nOctoroks!",
"Hardhat Beatle won't Let It Be?\n\nTell it to Get Back or give it a Ticket to Ride down a hole!", "I consoled\nFreezor over a\ncup of coffee.\nHis problems\njust seemed to\nmelt away!",
"Magic droplets\nof water don't\nshut up.\nThey just\nKyameron!",
"I bought hot\nwings for\nSluggula.\nThey gave him\nexplosive\ndiarrhea!",
"Hardhat Beetle\nwon't\nLet It Be?\nTell it to Get\nBack or give\nit a Ticket to\nRide down\na hole!",
] ]
junk_texts = [ junk_texts = [
@@ -344,15 +348,16 @@ junk_texts = [
] ]
KingsReturn_texts = [ KingsReturn_texts = [
"Who is this even", 'Who is this even',
'When did he get here', 'When did he get here',
'The Harem'
] * 2 + [ ] * 2 + [
"The Return of the King", "the return of the king",
"Fellowship of the Ring", "fellowship of the ring",
"The Two Towers", "the two towers",
] ]
Sanctuary_texts = [ Sanctuary_texts = [
"a priest's love" 'A Priest\'s love'
] * 2 + [ ] * 2 + [
"the loyal priest", "the loyal priest",
"read a book", "read a book",
@@ -370,30 +375,32 @@ Sahasrahla_names = [
Kakariko_texts = ["{}'s homecoming"] Kakariko_texts = ["{}'s homecoming"]
Blacksmiths_texts = [ Blacksmiths_texts = [
"Frogs for bread", 'frogs for bread',
"That's not a sword", 'That\'s not a sword',
"The rupeesmiths", 'The Rupeesmiths'
"The dwarven breadsmiths", ] * 1 + [
"the dwarven breadsmiths"
] ]
DeathMountain_texts = [ DeathMountain_texts = [
"The lost old man", "the lost old man",
"Gary the old man", "gary the old man",
"Your ad here", "Your ad here"
] ]
LostWoods_texts = [ LostWoods_texts = [
"Thieves' stump", 'thieves\' stump',
"Thieves' stump", 'He\'s got wood',
"The forest thief", ] * 2 + [
"Dancing pickles", "the forest thief",
"Flying crows", "dancing pickles",
"flying crows",
] ]
WishingWell_texts = [ WishingWell_texts = [
"Venus, Queen of Faeries", "venus. queen of faeries",
"Venus was her name", "Venus was her name",
"I'm your Venus", "I'm your Venus",
"Yeah, baby, she's got it", "Yeah, baby, shes got it",
"Venus, I'm your fire", "Venus, I'm your fire",
"Venus, at your desire", "Venus, At your desire",
"Venus Love Chain", "Venus Love Chain",
"Venus Crescent Beam", "Venus Crescent Beam",
] ]
@@ -482,7 +489,7 @@ class Credits(object):
], ],
'pedestal': [ 'pedestal': [
SceneSmallCreditLine(19, 'and the master sword'), SceneSmallCreditLine(19, 'and the master sword'),
SceneSmallAltCreditLine(21, 'sleeps again…'), SceneSmallAltCreditLine(21, 'sleeps again···'),
SceneLargeCreditLine(23, 'Forever!'), SceneLargeCreditLine(23, 'Forever!'),
], ],
} }
@@ -826,8 +833,6 @@ class CharTextMapper(object):
class RawMBTextMapper(CharTextMapper): class RawMBTextMapper(CharTextMapper):
char_map = {' ': 0xFF, char_map = {' ': 0xFF,
'/': 0x97,
'•': 0x98,
'≥': 0x99, # Cursor '≥': 0x99, # Cursor
'『': 0xC4, '『': 0xC4,
'』': 0xC5, '』': 0xC5,
@@ -1311,8 +1316,7 @@ class GoldCreditMapper(CharTextMapper):
class GreenCreditMapper(CharTextMapper): class GreenCreditMapper(CharTextMapper):
char_map = {' ': 0x9F, char_map = {' ': 0x9F,
'…': 0xFD, '.': 0x52,
'.': 0xFE,
'·': 0x52} '·': 0x52}
alpha_offset = -0x29 alpha_offset = -0x29
alpha_lower_offset = -0x29 alpha_lower_offset = -0x29
@@ -1762,7 +1766,7 @@ class TextTable(object):
text['sign_catfish'] = CompressedTextMapper.convert("Toss rocks\nToss items\nToss cookies") text['sign_catfish'] = CompressedTextMapper.convert("Toss rocks\nToss items\nToss cookies")
text['sign_north_village_of_outcasts'] = CompressedTextMapper.convert("↑ Skull Woods\n\n↓ Steve's Town") text['sign_north_village_of_outcasts'] = CompressedTextMapper.convert("↑ Skull Woods\n\n↓ Steve's Town")
text['sign_south_of_bumper_cave'] = CompressedTextMapper.convert("\n→ Karkats cave") text['sign_south_of_bumper_cave'] = CompressedTextMapper.convert("\n→ Karkats cave")
text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n→ Palace of Darkness") text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n→ Dark Palace")
text['sign_east_of_bomb_shop'] = CompressedTextMapper.convert("\n← Bomb Shoppe") text['sign_east_of_bomb_shop'] = CompressedTextMapper.convert("\n← Bomb Shoppe")
text['sign_east_of_mire'] = CompressedTextMapper.convert("\n← Misery Mire\n no way in.\n no way out.") text['sign_east_of_mire'] = CompressedTextMapper.convert("\n← Misery Mire\n no way in.\n no way out.")
text['sign_village_of_outcasts'] = CompressedTextMapper.convert("Have a Trulie Awesome Day!") text['sign_village_of_outcasts'] = CompressedTextMapper.convert("Have a Trulie Awesome Day!")
@@ -1797,7 +1801,6 @@ class TextTable(object):
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" text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n"
" ~~~2025~~~\nGammachuu\n\n"
" ~~~2024~~~\nGammachuu\n\n" " ~~~2024~~~\nGammachuu\n\n"
" ~~~2023~~~\nGanonsGoneWild\n\n" " ~~~2023~~~\nGanonsGoneWild\n\n"
" ~~~2022~~~\nObscure\n\n" " ~~~2022~~~\nObscure\n\n"
@@ -1866,7 +1869,7 @@ class TextTable(object):
# 100 # 100
text['dark_sanctuary_no'] = CompressedTextMapper.convert("Then go away!") text['dark_sanctuary_no'] = CompressedTextMapper.convert("Then go away!")
text['dark_sanctuary_hint_1'] = CompressedTextMapper.convert("There is a thief in the desert, he can open creepy chests that follow you. But now that we have that out of the way, Do you like my hair? I've spent eons getting it this way.") text['dark_sanctuary_hint_1'] = CompressedTextMapper.convert("There is a thief in the desert, he can open creepy chests that follow you. But now that we have that out of the way, Do you like my hair? I've spent eons getting it this way.")
text['dark_sanctuary_yes'] = CompressedTextMapper.convert("With crystals 5 and 6, you can find a great fairy in the pyramid.\n\nFlomp Flomp, Whizzle Whomp") text['dark_sanctuary_yes'] = CompressedTextMapper.convert("With Crystals 5&6, you can find a great fairy in the pyramid.\n\nFlomp Flomp, Whizzle Whomp")
text['dark_sanctuary_hint_2'] = CompressedTextMapper.convert( text['dark_sanctuary_hint_2'] = CompressedTextMapper.convert(
"All I can say is that my life is pretty plain,\n" "All I can say is that my life is pretty plain,\n"
+ "I like watchin' the puddles gather rain,\n" + "I like watchin' the puddles gather rain,\n"
@@ -2054,6 +2057,5 @@ 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['mastersword_pedestal_goal'] = CompressedTextMapper.convert("To claim thy reward, you must present all 3 Pendants of Virtue.") text['mastersword_pedestal_goal'] = CompressedTextMapper.convert("To claim thy reward, you must present all 3 Pendants of Virtue.")
text['orange_potion_refill'] = CompressedTextMapper.convert("This rare orange potion will give you unlimited magic, but only until you die or quit the game.") text['end_pad_data'] = bytearray([0xfb])
text['end_pad_data'] = bytearray([0xFB])
text['terminator'] = bytearray([0xFF, 0xFF]) text['terminator'] = bytearray([0xFF, 0xFF])
+27 -29
View File
@@ -8,7 +8,6 @@ import urllib.parse
import urllib.request import urllib.request
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from collections import defaultdict from collections import defaultdict
from hashlib import md5
from itertools import count from itertools import count
from math import factorial from math import factorial
from pathlib import Path from pathlib import Path
@@ -103,13 +102,31 @@ def close_console():
pass pass
def get_new_romhash(new_rom='working.sfc'): 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())
with open(new_rom, 'rb') as stream: with open(new_rom, 'rb') as stream:
new_rom_data = bytearray(stream.read()) new_rom_data = bytearray(stream.read())
# extend to 2 mb
old_rom_data.extend(bytearray([0x00] * (2097152 - len(old_rom_data))))
basemd5 = md5() 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.update(new_rom_data) basemd5.update(new_rom_data)
return basemd5.hexdigest() return "New Rom Hash: " + basemd5.hexdigest()
def kth_combination(k, l, r): def kth_combination(k, l, r):
@@ -356,26 +373,6 @@ def update_deprecated_args(args):
else: else:
args.shuffleganon = not args.no_shuffleganon in truthy args.shuffleganon = not args.no_shuffleganon in truthy
# OW Parallel defaults to TRUE
# Don't do: Yes
# Do: No
if "ow_unparallel" in argVars:
if isinstance(args.ow_parallel, dict):
for player in range(1, players + 1):
args.ow_parallel[player] = not args.ow_unparallel in truthy
else:
args.ow_parallel = not args.ow_unparallel in truthy
# OW Fog defaults to TRUE
# Don't do: Yes
# Do: No
if "ow_no_fog" in argVars:
if isinstance(args.ow_fog, dict):
for player in range(1, players + 1):
args.ow_fog[player] = not args.ow_no_fog in truthy
else:
args.ow_fog = not args.ow_no_fog in truthy
# Playthrough defaults to TRUE # Playthrough defaults to TRUE
# Don't do: Yes # Don't do: Yes
# Do: No # Do: No
@@ -771,18 +768,18 @@ class bidict(dict):
super(bidict, self).__init__(*args, **kwargs) super(bidict, self).__init__(*args, **kwargs)
self.inverse = {} self.inverse = {}
for key, value in self.items(): for key, value in self.items():
self.inverse.setdefault(value,[]).append(key) self.inverse.setdefault(value,[]).append(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if key in self: if key in self:
self.inverse[self[key]].remove(key) self.inverse[self[key]].remove(key)
super(bidict, self).__setitem__(key, value) super(bidict, self).__setitem__(key, value)
self.inverse.setdefault(value,[]).append(key) self.inverse.setdefault(value,[]).append(key)
def __delitem__(self, key): def __delitem__(self, key):
value = self[key] value = self[key]
self.inverse.setdefault(value,[]).remove(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] del self.inverse[value]
super(bidict, self).__delitem__(key) super(bidict, self).__delitem__(key)
@@ -790,11 +787,12 @@ class bidict(dict):
class HexInt(int): pass class HexInt(int): pass
def hex_representer(dumper, data): def hex_representer(dumper, data):
import yaml
return yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#0{4}x}") return yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#0{4}x}")
if __name__ == '__main__': if __name__ == '__main__':
print("New Rom Hash:", get_new_romhash()) print(make_new_base2current())
# 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])
+3 -3
View File
@@ -1,3 +1,3 @@
GKVersion = '1.1.0' GKVersion = '1.0.0'
ORVersion = '0.7.0.3' ORVersion = '0.6.1.7'
DRVersion = '1.5.6-u' DRVersion = '1.5.0-u'
+1 -139
View File
@@ -1,139 +1 @@
# Site settings theme: jekyll-theme-slate
title: ALttP Dungeon Randomizer
description: Advanced dungeon randomization for The Legend of Zelda - A Link to the Past
baseurl: "/ALttPDoorRandomizer"
url: "https://aerinon.github.io"
# Theme
theme: jekyll-theme-slate
remote_theme: just-the-docs/just-the-docs
# Logo and branding
# logo: "/assets/images/logo.png"
# Navigation
nav_enabled: true
nav_external_links:
- title: GitHub
url: https://github.com/aerinon/ALttPDoorRandomizer
- title: Discord
url: https://discordapp.com/invite/alttprandomizer
- title: Download
url: https://github.com/aerinon/ALttPDoorRandomizer/releases
# Search
search_enabled: true
search:
heading_level: 2
previews: 3
preview_words_before: 5
preview_words_after: 10
tokenizer_separator: /[\s/]+/
rel_url: true
button: true
# Heading anchors
heading_anchors: true
# Aux links
aux_links:
"Door Randomizer on GitHub":
- "https://github.com/aerinon/ALttPDoorRandomizer"
"ALTTP Rando Discord":
- "https://discordapp.com/invite/alttprandomizer"
aux_links_new_tab: true
# Footer
footer_content: "Copyright &copy; 2026 ALttP Door Randomizer Team. Based on ALttP Entrance Randomizer."
# Back to top link
back_to_top: true
back_to_top_text: "Back to top"
# Collections for blog posts
collections:
posts:
output: true
permalink: /blog/:year/:month/:day/:title/
# Defaults
defaults:
- scope:
path: ""
type: "posts"
values:
layout: "default"
- scope:
path: ""
values:
layout: "default"
# Build settings
markdown: kramdown
kramdown:
input: GFM
syntax_highlighter: rouge
syntax_highlighter_opts:
css_class: 'highlight'
plugins:
- jekyll-feed
- jekyll-seo-tag
- jekyll-sitemap
# Exclude from processing
exclude:
- .git/
- .github/
- .gitignore
- .gitattributes
- source/
- test/
- resources/
- data/
- debug/
- analysis/
- analysis2/
- alttp_dungeon_randomizer.egg-info/
- DR/
- "*.py"
- "*.pyc"
- "*.pyd"
- "*.pyo"
- "*.egg-info"
- "*.spec"
- requirements*.txt
- setup.py
- TestSuite.py
- DungeonRandomizer.py
- Gui.py
- MultiServer.py
- MultiClient.py
- CLAUDE.md
- LICENSE
- libbz2-dev
- tlogbfs.txt
# Include files
include:
- _posts
- blog
- docs/Customizer.md
- docs/BUILDING.md
- docs/DR_hint_reference.md
# Color scheme (for just-the-docs theme)
color_scheme: dark
# Google Analytics (optional - add your tracking ID)
# ga_tracking: UA-XXXXXXXX-X
# Compress HTML
compress_html:
clippings: all
comments: all
endings: all
startings: []
blanklines: false
profile: false
@@ -1,74 +0,0 @@
---
layout: default
title: "Welcome to the ALttP Door Randomizer Blog"
date: 2026-01-26
author: Door Randomizer Team
category: development
excerpt: "Introducing the new Door Randomizer documentation site and blog. Learn about what's new, what's coming, and how to get involved."
---
# Welcome to the ALttP Door Randomizer Blog
We're excited to launch the official documentation site and development blog for ALttP Door Randomizer! This site serves as a central hub for all things Door Rando - from comprehensive feature documentation to release notes and development updates.
## What's New Here?
### Comprehensive Documentation
We've reorganized and expanded our documentation to make it easier to find what you need:
- **[Features Guide](/features)** - Complete reference for all randomizer settings and modes
- **[Installation & Usage](/installation)** - Step-by-step setup instructions for all platforms
- **[Known Issues](/known-issues)** - Tracking current bugs and limitations
- **[Roadmap](/roadmap)** - Our vision for the future of Door Randomizer
### Development Blog
This blog will be your source for:
- **Feature highlights** - Deep dives into specific features
- **Development updates** - Behind-the-scenes looks at ongoing work
- **Technical articles** - How the randomizer works under the hood
## What is Door Randomizer?
If you're new here, Door Randomizer takes the ALttP randomizer experience to the next level by shuffling the connections between rooms within dungeons. Walk through a door in Eastern Palace and you might end up in Tower of Hera, Ice Palace, or anywhere else!
Key features include:
- **Four shuffle modes** - From basic (within dungeon) to crossed (full chaos)
- **Three intensity levels** - Control how thoroughly dungeons are shuffled
- **Advanced key logic** - Multiple algorithms to prevent self-locking
- **Pottery shuffle** - Randomize items under pots with multiple modes
- **Shopsanity** - 32+ shop locations with intelligent pricing
- **Enemizer integration** - Enemy and boss randomization with logic
- **Custom seeds** - Create your own dungeons and rooms
[Learn more about features →](/features)
[Read the Customizer guide →](https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/Customizer.md)
## What's Coming Next?
We have plans for Door Randomizer's future. Check out our [Roadmap](/roadmap) for details. (Still very much a work in progress!)
## Community
Join the Door Randomizer community:
- **Discord**: [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer)
- `#door-rando` - General discussion and help
- `#bug-reports` - Report bugs and issues
- **GitHub**: [aerinon/ALttPDoorRandomizer](https://github.com/aerinon/ALttPDoorRandomizer)
## Stay Tuned
We'll be posting irregular updates here as development continues.
Thank you to everyone who has contributed to Door Randomizer through code, testing, feedback, and community support. We're excited about the future and can't wait to see what lies in store!
Happy randomizing!
---
*Have questions or feedback about the new documentation site? Let us know in Discord!*
View File
@@ -0,0 +1,5 @@
Mike Lenzen <m.lenzen@gmail.com> https://github.com/mlenzen
Caleb Levy <caleb.levy@berkeley.edu> https://github.com/caleblevy
Marein Könings <mail@marein.org> https://github.com/MareinK
Jad Kik <jadkik94@gmail.com> https://github.com/jadkik
Kuba Marek <blue.cube@seznam.cz> https://github.com/bluecube
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+55
View File
@@ -0,0 +1,55 @@
"""collections_extended contains a few extra basic data structures."""
from ._compat import Collection
from .bags import bag, frozenbag
from .bijection import bijection
from .range_map import MappedRange, RangeMap
from .setlists import frozensetlist, setlist
__version__ = '1.0.2'
__all__ = (
'collection',
'setlist',
'frozensetlist',
'bag',
'frozenbag',
'bijection',
'RangeMap',
'MappedRange',
'Collection',
)
def collection(iterable=None, mutable=True, ordered=False, unique=False):
"""Return a Collection with the specified properties.
Args:
iterable (Iterable): collection to instantiate new collection from.
mutable (bool): Whether or not the new collection is mutable.
ordered (bool): Whether or not the new collection is ordered.
unique (bool): Whether or not the new collection contains only unique values.
"""
if iterable is None:
iterable = tuple()
if unique:
if ordered:
if mutable:
return setlist(iterable)
else:
return frozensetlist(iterable)
else:
if mutable:
return set(iterable)
else:
return frozenset(iterable)
else:
if ordered:
if mutable:
return list(iterable)
else:
return tuple(iterable)
else:
if mutable:
return bag(iterable)
else:
return frozenbag(iterable)
+53
View File
@@ -0,0 +1,53 @@
"""Python 2/3 compatibility helpers."""
import sys
is_py2 = sys.version_info[0] == 2
if is_py2:
def keys_set(d):
"""Return a set of passed dictionary's keys."""
return set(d.keys())
else:
keys_set = dict.keys
if sys.version_info < (3, 6):
from collections import Container, Iterable, Sized
def _check_methods(C, *methods):
mro = C.__mro__
for method in methods:
for B in mro:
if method in B.__dict__:
if B.__dict__[method] is None:
return NotImplemented
break
else:
return NotImplemented
return True
class Collection(Sized, Iterable, Container):
"""Backport from Python3.6."""
__slots__ = tuple()
@classmethod
def __subclasshook__(cls, C):
if cls is Collection:
return _check_methods(C, "__len__", "__iter__", "__contains__")
return NotImplemented
else:
from collections.abc import Collection
def handle_rich_comp_not_implemented():
"""Correctly handle unimplemented rich comparisons.
In Python 3, return NotImplemented.
In Python 2, raise a TypeError.
"""
if is_py2:
raise TypeError()
else:
return NotImplemented
+16
View File
@@ -0,0 +1,16 @@
"""util functions for collections_extended."""
def hash_iterable(it):
"""Perform a O(1) memory hash of an iterable of arbitrary length.
hash(tuple(it)) creates a temporary tuple containing all values from it
which could be a problem if it is large.
See discussion at:
https://groups.google.com/forum/#!msg/python-ideas/XcuC01a8SYs/e-doB9TbDwAJ
"""
hash_value = hash(type(it))
for value in it:
hash_value = hash((hash_value, value))
return hash_value
+527
View File
@@ -0,0 +1,527 @@
"""Bag class definitions."""
import heapq
from collections import Hashable, MutableSet, Set
from operator import itemgetter
from . import _compat
class _basebag(Set):
"""Base class for bag classes.
Base class for bag and frozenbag. Is not mutable and not hashable, so there's
no reason to use this instead of either bag or frozenbag.
"""
# Basic object methods
def __init__(self, iterable=None):
"""Create a new basebag.
If iterable isn't given, is None or is empty then the bag starts empty.
Otherwise each element from iterable will be added to the bag
however many times it appears.
This runs in O(len(iterable))
"""
self._dict = dict()
self._size = 0
if iterable:
if isinstance(iterable, _basebag):
for elem, count in iterable._dict.items():
self._dict[elem] = count
self._size += count
else:
for value in iterable:
self._dict[value] = self._dict.get(value, 0) + 1
self._size += 1
def __repr__(self):
if self._size == 0:
return '{0}()'.format(self.__class__.__name__)
else:
repr_format = '{class_name}({values!r})'
return repr_format.format(
class_name=self.__class__.__name__,
values=tuple(self),
)
def __str__(self):
if self._size == 0:
return '{class_name}()'.format(class_name=self.__class__.__name__)
else:
format_single = '{elem!r}'
format_mult = '{elem!r}^{mult}'
strings = []
for elem, mult in self._dict.items():
if mult > 1:
strings.append(format_mult.format(elem=elem, mult=mult))
else:
strings.append(format_single.format(elem=elem))
return '{%s}' % ', '.join(strings)
# New public methods (not overriding/implementing anything)
def num_unique_elements(self):
"""Return the number of unique elements.
This runs in O(1) time
"""
return len(self._dict)
def unique_elements(self):
"""Return a view of unique elements in this bag.
In Python 3:
This runs in O(1) time and returns a view of the unique elements
In Python 2:
This runs in O(n) and returns set of the current elements.
"""
return _compat.keys_set(self._dict)
def count(self, value):
"""Return the number of value present in this bag.
If value is not in the bag no Error is raised, instead 0 is returned.
This runs in O(1) time
Args:
value: The element of self to get the count of
Returns:
int: The count of value in self
"""
return self._dict.get(value, 0)
def nlargest(self, n=None):
"""List the n most common elements and their counts.
List is from the most
common to the least. If n is None, the list all element counts.
Run time should be O(m log m) where m is len(self)
Args:
n (int): The number of elements to return
"""
if n is None:
return sorted(self._dict.items(), key=itemgetter(1), reverse=True)
else:
return heapq.nlargest(n, self._dict.items(), key=itemgetter(1))
@classmethod
def _from_iterable(cls, it):
return cls(it)
@classmethod
def from_mapping(cls, mapping):
"""Create a bag from a dict of elem->count.
Each key in the dict is added if the value is > 0.
"""
out = cls()
for elem, count in mapping.items():
if count > 0:
out._dict[elem] = count
out._size += count
return out
def copy(self):
"""Create a shallow copy of self.
This runs in O(len(self.num_unique_elements()))
"""
return self.from_mapping(self._dict)
# implementing Sized methods
def __len__(self):
"""Return the cardinality of the bag.
This runs in O(1)
"""
return self._size
# implementing Container methods
def __contains__(self, value):
"""Return the multiplicity of the element.
This runs in O(1)
"""
return self._dict.get(value, 0)
# implementing Iterable methods
def __iter__(self):
"""Iterate through all elements.
Multiple copies will be returned if they exist.
"""
for value, count in self._dict.items():
for i in range(count):
yield(value)
# Comparison methods
def _is_subset(self, other):
"""Check that every element in self has a count <= in other.
Args:
other (Set)
"""
if isinstance(other, _basebag):
for elem, count in self._dict.items():
if not count <= other._dict.get(elem, 0):
return False
else:
for elem in self:
if self._dict.get(elem, 0) > 1 or elem not in other:
return False
return True
def _is_superset(self, other):
"""Check that every element in self has a count >= in other.
Args:
other (Set)
"""
if isinstance(other, _basebag):
for elem, count in other._dict.items():
if not self._dict.get(elem, 0) >= count:
return False
else:
for elem in other:
if elem not in self:
return False
return True
def __le__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) <= len(other) and self._is_subset(other)
def __lt__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) < len(other) and self._is_subset(other)
def __gt__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) > len(other) and self._is_superset(other)
def __ge__(self, other):
if not isinstance(other, Set):
return _compat.handle_rich_comp_not_implemented()
return len(self) >= len(other) and self._is_superset(other)
def __eq__(self, other):
if not isinstance(other, Set):
return False
if isinstance(other, _basebag):
return self._dict == other._dict
if not len(self) == len(other):
return False
for elem in other:
if self._dict.get(elem, 0) != 1:
return False
return True
def __ne__(self, other):
return not (self == other)
# Operations - &, |, +, -, ^, * and isdisjoint
def __and__(self, other):
"""Intersection is the minimum of corresponding counts.
This runs in O(l + n) where:
n is self.num_unique_elements()
if other is a bag:
l = 1
else:
l = len(other)
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
values = dict()
for elem in self._dict:
values[elem] = min(other._dict.get(elem, 0), self._dict.get(elem, 0))
return self.from_mapping(values)
def isdisjoint(self, other):
"""Return if this bag is disjoint with the passed collection.
This runs in O(len(other))
TODO move isdisjoint somewhere more appropriate
"""
for value in other:
if value in self:
return False
return True
def __or__(self, other):
"""Union is the maximum of all elements.
This runs in O(m + n) where:
n is self.num_unique_elements()
if other is a bag:
m = other.num_unique_elements()
else:
m = len(other)
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
values = dict()
for elem in self.unique_elements() | other.unique_elements():
values[elem] = max(self._dict.get(elem, 0), other._dict.get(elem, 0))
return self.from_mapping(values)
def __add__(self, other):
"""Return a new bag also containing all the elements of other.
self + other = self & other + self | other
This runs in O(m + n) where:
n is self.num_unique_elements()
m is len(other)
Args:
other (Iterable): elements to add to self
"""
out = self.copy()
for value in other:
out._dict[value] = out._dict.get(value, 0) + 1
out._size += 1
return out
def __sub__(self, other):
"""Difference between the sets.
For normal sets this is all x s.t. x in self and x not in other.
For bags this is count(x) = max(0, self.count(x)-other.count(x))
This runs in O(m + n) where:
n is self.num_unique_elements()
m is len(other)
Args:
other (Iterable): elements to remove
"""
out = self.copy()
for value in other:
old_count = out._dict.get(value, 0)
if old_count == 1:
del out._dict[value]
out._size -= 1
elif old_count > 1:
out._dict[value] = old_count - 1
out._size -= 1
return out
def __mul__(self, other):
"""Cartesian product of the two sets.
other can be any iterable.
Both self and other must contain elements that can be added together.
This should run in O(m*n+l) where:
m is the number of unique elements in self
n is the number of unique elements in other
if other is a bag:
l is 0
else:
l is the len(other)
The +l will only really matter when other is an iterable with MANY
repeated elements.
For example: {'a'^2} * 'bbbbbbbbbbbbbbbbbbbbbbbbbb'
The algorithm will be dominated by counting the 'b's
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
values = dict()
for elem, count in self._dict.items():
for other_elem, other_count in other._dict.items():
new_elem = elem + other_elem
new_count = count * other_count
values[new_elem] = new_count
return self.from_mapping(values)
def __xor__(self, other):
"""Symmetric difference between the sets.
other can be any iterable.
This runs in O(m + n) where:
m = len(self)
n = len(other)
"""
return (self - other) | (other - self)
class bag(_basebag, MutableSet):
"""bag is a mutable unhashable bag."""
def pop(self):
"""Remove and return an element of self."""
# TODO can this be done more efficiently (no need to create an iterator)?
it = iter(self)
try:
value = next(it)
except StopIteration:
raise KeyError
self.discard(value)
return value
def add(self, elem):
"""Add elem to self."""
self._dict[elem] = self._dict.get(elem, 0) + 1
self._size += 1
def discard(self, elem):
"""Remove elem from this bag, silent if it isn't present."""
try:
self.remove(elem)
except ValueError:
pass
def remove(self, elem):
"""Remove elem from this bag, raising a ValueError if it isn't present.
Args:
elem: object to remove from self
Raises:
ValueError: if the elem isn't present
"""
old_count = self._dict.get(elem, 0)
if old_count == 0:
raise ValueError
elif old_count == 1:
del self._dict[elem]
else:
self._dict[elem] -= 1
self._size -= 1
def discard_all(self, other):
"""Discard all of the elems from other."""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, other_count in other._dict.items():
old_count = self._dict.get(elem, 0)
new_count = old_count - other_count
if new_count >= 0:
if new_count == 0:
if elem in self:
del self._dict[elem]
else:
self._dict[elem] = new_count
self._size += new_count - old_count
def remove_all(self, other):
"""Remove all of the elems from other.
Raises a ValueError if the multiplicity of any elem in other is greater
than in self.
"""
if not self._is_superset(other):
raise ValueError
self.discard_all(other)
def clear(self):
"""Remove all elements from this bag."""
self._dict = dict()
self._size = 0
# In-place operations
def __ior__(self, other):
"""Set multiplicity of each element to the maximum of the two collections.
if isinstance(other, _basebag):
This runs in O(other.num_unique_elements())
else:
This runs in O(len(other))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, other_count in other._dict.items():
old_count = self._dict.get(elem, 0)
new_count = max(other_count, old_count)
self._dict[elem] = new_count
self._size += new_count - old_count
return self
def __iand__(self, other):
"""Set multiplicity of each element to the minimum of the two collections.
if isinstance(other, _basebag):
This runs in O(other.num_unique_elements())
else:
This runs in O(len(other))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, old_count in set(self._dict.items()):
other_count = other._dict.get(elem, 0)
new_count = min(other_count, old_count)
if new_count == 0:
del self._dict[elem]
else:
self._dict[elem] = new_count
self._size += new_count - old_count
return self
def __ixor__(self, other):
"""Set self to the symmetric difference between the sets.
if isinstance(other, _basebag):
This runs in O(other.num_unique_elements())
else:
This runs in O(len(other))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
other_minus_self = other - self
self -= other
self |= other_minus_self
return self
def __isub__(self, other):
"""Discard the elements of other from self.
if isinstance(it, _basebag):
This runs in O(it.num_unique_elements())
else:
This runs in O(len(it))
"""
self.discard_all(other)
return self
def __iadd__(self, other):
"""Add all of the elements of other to self.
if isinstance(it, _basebag):
This runs in O(it.num_unique_elements())
else:
This runs in O(len(it))
"""
if not isinstance(other, _basebag):
other = self._from_iterable(other)
for elem, other_count in other._dict.items():
self._dict[elem] = self._dict.get(elem, 0) + other_count
self._size += other_count
return self
class frozenbag(_basebag, Hashable):
"""frozenbag is an immutable, hashable bab."""
def __hash__(self):
"""Compute the hash value of a frozenbag.
This was copied directly from _collections_abc.Set._hash in Python3 which
is identical to _abcoll.Set._hash
We can't call it directly because Python2 raises a TypeError.
"""
if not hasattr(self, '_hash_value'):
self._hash_value = self._hash()
return self._hash_value
+94
View File
@@ -0,0 +1,94 @@
"""Class definition for bijection."""
from collections import Mapping, MutableMapping
class bijection(MutableMapping):
"""A one-to-one onto mapping, a dict with unique values."""
def __init__(self, iterable=None, **kwarg):
"""Create a bijection from an iterable.
Matches dict.__init__.
"""
self._data = {}
self.__inverse = self.__new__(bijection)
self.__inverse._data = {}
self.__inverse.__inverse = self
if iterable is not None:
if isinstance(iterable, Mapping):
for key, value in iterable.items():
self[key] = value
else:
for pair in iterable:
self[pair[0]] = pair[1]
for key, value in kwarg.items():
self[key] = value
def __repr__(self):
if len(self._data) == 0:
return '{0}()'.format(self.__class__.__name__)
else:
repr_format = '{class_name}({values!r})'
return repr_format.format(
class_name=self.__class__.__name__,
values=self._data,
)
@property
def inverse(self):
"""Return the inverse of this bijection."""
return self.__inverse
# Required for MutableMapping
def __len__(self):
return len(self._data)
# Required for MutableMapping
def __getitem__(self, key):
return self._data[key]
# Required for MutableMapping
def __setitem__(self, key, value):
if key in self:
del self.inverse._data[self[key]]
if value in self.inverse:
del self._data[self.inverse[value]]
self._data[key] = value
self.inverse._data[value] = key
# Required for MutableMapping
def __delitem__(self, key):
value = self._data.pop(key)
del self.inverse._data[value]
# Required for MutableMapping
def __iter__(self):
return iter(self._data)
def __contains__(self, key):
return key in self._data
def clear(self):
"""Remove everything from this bijection."""
self._data.clear()
self.inverse._data.clear()
def copy(self):
"""Return a copy of this bijection."""
return bijection(self)
def items(self):
"""See Mapping.items."""
return self._data.items()
def keys(self):
"""See Mapping.keys."""
return self._data.keys()
def values(self):
"""See Mapping.values."""
return self.inverse.keys()
def __eq__(self, other):
return isinstance(other, bijection) and self._data == other._data
+383
View File
@@ -0,0 +1,383 @@
"""RangeMap class definition."""
from bisect import bisect_left, bisect_right
from collections import Mapping, MappingView, Set, namedtuple
# Used to mark unmapped ranges
_empty = object()
MappedRange = namedtuple('MappedRange', ('start', 'stop', 'value'))
class KeysView(MappingView, Set):
"""A view of the keys that mark the starts of subranges.
Since iterating over all the keys is impossible, the KeysView only
contains the keys that start each subrange.
"""
__slots__ = ()
@classmethod
def _from_iterable(self, it):
return set(it)
def __contains__(self, key):
loc = self._mapping._bisect_left(key)
return self._mapping._keys[loc] == key and \
self._mapping._values[loc] is not _empty
def __iter__(self):
for item in self._mapping.ranges():
yield item.start
class ItemsView(MappingView, Set):
"""A view of the items that mark the starts of subranges.
Since iterating over all the keys is impossible, the ItemsView only
contains the items that start each subrange.
"""
__slots__ = ()
@classmethod
def _from_iterable(self, it):
return set(it)
def __contains__(self, item):
key, value = item
loc = self._mapping._bisect_left(key)
return self._mapping._keys[loc] == key and \
self._mapping._values[loc] == value
def __iter__(self):
for mapped_range in self._mapping.ranges():
yield (mapped_range.start, mapped_range.value)
class ValuesView(MappingView):
"""A view on the values of a Mapping."""
__slots__ = ()
def __contains__(self, value):
return value in self._mapping._values
def __iter__(self):
for value in self._mapping._values:
if value is not _empty:
yield value
def _check_start_stop(start, stop):
"""Check that start and stop are valid - orderable and in the right order.
Raises:
ValueError: if stop <= start
TypeError: if unorderable
"""
if start is not None and stop is not None and stop <= start:
raise ValueError('stop must be > start')
def _check_key_slice(key):
if not isinstance(key, slice):
raise TypeError('Can only set and delete slices')
if key.step is not None:
raise ValueError('Cannot set or delete slices with steps')
class RangeMap(Mapping):
"""Map ranges of orderable elements to values."""
def __init__(self, iterable=None, **kwargs):
"""Create a RangeMap.
A mapping or other iterable can be passed to initialize the RangeMap.
If mapping is passed, it is interpreted as a mapping from range start
indices to values.
If an iterable is passed, each element will define a range in the
RangeMap and should be formatted (start, stop, value).
default_value is a an optional keyword argument that will initialize the
entire RangeMap to that value. Any missing ranges will be mapped to that
value. However, if ranges are subsequently deleted they will be removed
and *not* mapped to the default_value.
Args:
iterable: A Mapping or an Iterable to initialize from.
default_value: If passed, the return value for all keys less than the
least key in mapping or missing ranges in iterable. If no mapping
or iterable, the return value for all keys.
"""
default_value = kwargs.pop('default_value', _empty)
if kwargs:
raise TypeError('Unknown keyword arguments: %s' % ', '.join(kwargs.keys()))
self._keys = [None]
self._values = [default_value]
if iterable:
if isinstance(iterable, Mapping):
self._init_from_mapping(iterable)
else:
self._init_from_iterable(iterable)
@classmethod
def from_mapping(cls, mapping):
"""Create a RangeMap from a mapping of interval starts to values."""
obj = cls()
obj._init_from_mapping(mapping)
return obj
def _init_from_mapping(self, mapping):
for key, value in sorted(mapping.items()):
self.set(value, key)
@classmethod
def from_iterable(cls, iterable):
"""Create a RangeMap from an iterable of tuples defining each range.
Each element of the iterable is a tuple (start, stop, value).
"""
obj = cls()
obj._init_from_iterable(iterable)
return obj
def _init_from_iterable(self, iterable):
for start, stop, value in iterable:
self.set(value, start=start, stop=stop)
def __str__(self):
range_format = '({range.start}, {range.stop}): {range.value}'
values = ', '.join([range_format.format(range=r) for r in self.ranges()])
return 'RangeMap(%s)' % values
def __repr__(self):
range_format = '({range.start!r}, {range.stop!r}, {range.value!r})'
values = ', '.join([range_format.format(range=r) for r in self.ranges()])
return 'RangeMap([%s])' % values
def _bisect_left(self, key):
"""Return the index of the key or the last key < key."""
if key is None:
return 0
else:
return bisect_left(self._keys, key, lo=1)
def _bisect_right(self, key):
"""Return the index of the first key > key."""
if key is None:
return 1
else:
return bisect_right(self._keys, key, lo=1)
def ranges(self, start=None, stop=None):
"""Generate MappedRanges for all mapped ranges.
Yields:
MappedRange
"""
_check_start_stop(start, stop)
start_loc = self._bisect_right(start)
if stop is None:
stop_loc = len(self._keys)
else:
stop_loc = self._bisect_left(stop)
start_val = self._values[start_loc - 1]
candidate_keys = [start] + self._keys[start_loc:stop_loc] + [stop]
candidate_values = [start_val] + self._values[start_loc:stop_loc]
for i, value in enumerate(candidate_values):
if value is not _empty:
start_key = candidate_keys[i]
stop_key = candidate_keys[i + 1]
yield MappedRange(start_key, stop_key, value)
def __contains__(self, value):
try:
self.__getitem(value) is not _empty
except KeyError:
return False
else:
return True
def __iter__(self):
for key, value in zip(self._keys, self._values):
if value is not _empty:
yield key
def __bool__(self):
if len(self._keys) > 1:
return True
else:
return self._values[0] != _empty
__nonzero__ = __bool__
def __getitem(self, key):
"""Get the value for a key (not a slice)."""
loc = self._bisect_right(key) - 1
value = self._values[loc]
if value is _empty:
raise KeyError(key)
else:
return value
def get(self, key, restval=None):
"""Get the value of the range containing key, otherwise return restval."""
try:
return self.__getitem(key)
except KeyError:
return restval
def get_range(self, start=None, stop=None):
"""Return a RangeMap for the range start to stop.
Returns:
A RangeMap
"""
return self.from_iterable(self.ranges(start, stop))
def set(self, value, start=None, stop=None):
"""Set the range from start to stop to value."""
_check_start_stop(start, stop)
# start_index, stop_index will denote the sections we are replacing
start_index = self._bisect_left(start)
if start is not None: # start_index == 0
prev_value = self._values[start_index - 1]
if prev_value == value:
# We're setting a range where the left range has the same
# value, so create one big range
start_index -= 1
start = self._keys[start_index]
if stop is None:
new_keys = [start]
new_values = [value]
stop_index = len(self._keys)
else:
stop_index = self._bisect_right(stop)
stop_value = self._values[stop_index - 1]
stop_key = self._keys[stop_index - 1]
if stop_key == stop and stop_value == value:
new_keys = [start]
new_values = [value]
else:
new_keys = [start, stop]
new_values = [value, stop_value]
self._keys[start_index:stop_index] = new_keys
self._values[start_index:stop_index] = new_values
def delete(self, start=None, stop=None):
"""Delete the range from start to stop from self.
Raises:
KeyError: If part of the passed range isn't mapped.
"""
_check_start_stop(start, stop)
start_loc = self._bisect_right(start) - 1
if stop is None:
stop_loc = len(self._keys)
else:
stop_loc = self._bisect_left(stop)
for value in self._values[start_loc:stop_loc]:
if value is _empty:
raise KeyError((start, stop))
# this is inefficient, we've already found the sub ranges
self.set(_empty, start=start, stop=stop)
def empty(self, start=None, stop=None):
"""Empty the range from start to stop.
Like delete, but no Error is raised if the entire range isn't mapped.
"""
self.set(_empty, start=start, stop=stop)
def clear(self):
"""Remove all elements."""
self._keys = [None]
self._values = [_empty]
@property
def start(self):
"""Get the start key of the first range.
None if RangeMap is empty or unbounded to the left.
"""
if self._values[0] is _empty:
try:
return self._keys[1]
except IndexError:
# This is empty or everything is mapped to a single value
return None
else:
# This is unbounded to the left
return self._keys[0]
@property
def end(self):
"""Get the stop key of the last range.
None if RangeMap is empty or unbounded to the right.
"""
if self._values[-1] is _empty:
return self._keys[-1]
else:
# This is unbounded to the right
return None
def __eq__(self, other):
if isinstance(other, RangeMap):
return (
self._keys == other._keys and
self._values == other._values
)
else:
return False
def __getitem__(self, key):
try:
_check_key_slice(key)
except TypeError:
return self.__getitem(key)
else:
return self.get_range(key.start, key.stop)
def __setitem__(self, key, value):
_check_key_slice(key)
self.set(value, key.start, key.stop)
def __delitem__(self, key):
_check_key_slice(key)
self.delete(key.start, key.stop)
def __len__(self):
count = 0
for v in self._values:
if v is not _empty:
count += 1
return count
def keys(self):
"""Return a view of the keys."""
return KeysView(self)
def values(self):
"""Return a view of the values."""
return ValuesView(self)
def items(self):
"""Return a view of the item pairs."""
return ItemsView(self)
# Python2 - override slice methods
def __setslice__(self, i, j, value):
"""Implement __setslice__ to override behavior in Python 2.
This is required because empty slices pass integers in python2 as opposed
to None in python 3.
"""
raise SyntaxError('Assigning slices doesn\t work in Python 2, use set')
def __delslice__(self, i, j):
raise SyntaxError('Deleting slices doesn\t work in Python 2, use delete')
def __getslice__(self, i, j):
raise SyntaxError('Getting slices doesn\t work in Python 2, use get_range.')
+545
View File
@@ -0,0 +1,545 @@
"""Setlist class definitions."""
import random as random_
from collections import Hashable, MutableSequence, MutableSet, Sequence, Set
from . import _util
class _basesetlist(Sequence, Set):
"""A setlist is an ordered Collection of unique elements.
_basesetlist is the superclass of setlist and frozensetlist. It is immutable
and unhashable.
"""
def __init__(self, iterable=None, raise_on_duplicate=False):
"""Create a setlist.
Args:
iterable (Iterable): Values to initialize the setlist with.
"""
self._list = list()
self._dict = dict()
if iterable:
if raise_on_duplicate:
self._extend(iterable)
else:
self._update(iterable)
def __repr__(self):
if len(self) == 0:
return '{0}()'.format(self.__class__.__name__)
else:
repr_format = '{class_name}({values!r})'
return repr_format.format(
class_name=self.__class__.__name__,
values=tuple(self),
)
# Convenience methods
def _fix_neg_index(self, index):
if index < 0:
index += len(self)
if index < 0:
raise IndexError('index is out of range')
return index
def _fix_end_index(self, index):
if index is None:
return len(self)
else:
return self._fix_neg_index(index)
def _append(self, value):
# Checking value in self will check that value is Hashable
if value in self:
raise ValueError('Value "%s" already present' % str(value))
else:
self._dict[value] = len(self)
self._list.append(value)
def _extend(self, values):
new_values = set()
for value in values:
if value in new_values:
raise ValueError('New values contain duplicates')
elif value in self:
raise ValueError('New values contain elements already present in self')
else:
new_values.add(value)
for value in values:
self._dict[value] = len(self)
self._list.append(value)
def _add(self, item):
if item not in self:
self._dict[item] = len(self)
self._list.append(item)
def _update(self, values):
for value in values:
if value not in self:
self._dict[value] = len(self)
self._list.append(value)
@classmethod
def _from_iterable(cls, it, **kwargs):
return cls(it, **kwargs)
# Implement Container
def __contains__(self, value):
return value in self._dict
# Iterable we get by inheriting from Sequence
# Implement Sized
def __len__(self):
return len(self._list)
# Implement Sequence
def __getitem__(self, index):
if isinstance(index, slice):
return self._from_iterable(self._list[index])
return self._list[index]
def count(self, value):
"""Return the number of occurences of value in self.
This runs in O(1)
Args:
value: The value to count
Returns:
int: 1 if the value is in the setlist, otherwise 0
"""
if value in self:
return 1
else:
return 0
def index(self, value, start=0, end=None):
"""Return the index of value between start and end.
By default, the entire setlist is searched.
This runs in O(1)
Args:
value: The value to find the index of
start (int): The index to start searching at (defaults to 0)
end (int): The index to stop searching at (defaults to the end of the list)
Returns:
int: The index of the value
Raises:
ValueError: If the value is not in the list or outside of start - end
IndexError: If start or end are out of range
"""
try:
index = self._dict[value]
except KeyError:
raise ValueError
else:
start = self._fix_neg_index(start)
end = self._fix_end_index(end)
if start <= index and index < end:
return index
else:
raise ValueError
@classmethod
def _check_type(cls, other, operand_name):
if not isinstance(other, _basesetlist):
message = (
"unsupported operand type(s) for {operand_name}: "
"'{self_type}' and '{other_type}'").format(
operand_name=operand_name,
self_type=cls,
other_type=type(other),
)
raise TypeError(message)
def __add__(self, other):
self._check_type(other, '+')
out = self.copy()
out._extend(other)
return out
# Implement Set
def issubset(self, other):
return self <= other
def issuperset(self, other):
return self >= other
def union(self, other):
out = self.copy()
out.update(other)
return out
def intersection(self, other):
other = set(other)
return self._from_iterable(item for item in self if item in other)
def difference(self, other):
other = set(other)
return self._from_iterable(item for item in self if item not in other)
def symmetric_difference(self, other):
return self.union(other) - self.intersection(other)
def __sub__(self, other):
self._check_type(other, '-')
return self.difference(other)
def __and__(self, other):
self._check_type(other, '&')
return self.intersection(other)
def __or__(self, other):
self._check_type(other, '|')
return self.union(other)
def __xor__(self, other):
self._check_type(other, '^')
return self.symmetric_difference(other)
# Comparison
def __eq__(self, other):
if not isinstance(other, _basesetlist):
return False
if not len(self) == len(other):
return False
for self_elem, other_elem in zip(self, other):
if self_elem != other_elem:
return False
return True
def __ne__(self, other):
return not (self == other)
# New methods
def sub_index(self, sub, start=0, end=None):
"""Return the index of a subsequence.
This runs in O(len(sub))
Args:
sub (Sequence): An Iterable to search for
Returns:
int: The index of the first element of sub
Raises:
ValueError: If sub isn't a subsequence
TypeError: If sub isn't iterable
IndexError: If start or end are out of range
"""
start_index = self.index(sub[0], start, end)
end = self._fix_end_index(end)
if start_index + len(sub) > end:
raise ValueError
for i in range(1, len(sub)):
if sub[i] != self[start_index + i]:
raise ValueError
return start_index
def copy(self):
return self.__class__(self)
class setlist(_basesetlist, MutableSequence, MutableSet):
"""A mutable (unhashable) setlist."""
def __str__(self):
return '{[%s}]' % ', '.join(repr(v) for v in self)
# Helper methods
def _delete_all(self, elems_to_delete, raise_errors):
indices_to_delete = set()
for elem in elems_to_delete:
try:
elem_index = self._dict[elem]
except KeyError:
if raise_errors:
raise ValueError('Passed values contain elements not in self')
else:
if elem_index in indices_to_delete:
if raise_errors:
raise ValueError('Passed vales contain duplicates')
indices_to_delete.add(elem_index)
self._delete_values_by_index(indices_to_delete)
def _delete_values_by_index(self, indices_to_delete):
deleted_count = 0
for i, elem in enumerate(self._list):
if i in indices_to_delete:
deleted_count += 1
del self._dict[elem]
else:
new_index = i - deleted_count
self._list[new_index] = elem
self._dict[elem] = new_index
# Now remove deleted_count items from the end of the list
if deleted_count:
self._list = self._list[:-deleted_count]
# Set/Sequence agnostic
def pop(self, index=-1):
"""Remove and return the item at index."""
value = self._list.pop(index)
del self._dict[value]
return value
def clear(self):
"""Remove all elements from self."""
self._dict = dict()
self._list = list()
# Implement MutableSequence
def __setitem__(self, index, value):
if isinstance(index, slice):
old_values = self[index]
for v in value:
if v in self and v not in old_values:
raise ValueError
self._list[index] = value
self._dict = {}
for i, v in enumerate(self._list):
self._dict[v] = i
else:
index = self._fix_neg_index(index)
old_value = self._list[index]
if value in self:
if value == old_value:
return
else:
raise ValueError
del self._dict[old_value]
self._list[index] = value
self._dict[value] = index
def __delitem__(self, index):
if isinstance(index, slice):
indices_to_delete = set(self.index(e) for e in self._list[index])
self._delete_values_by_index(indices_to_delete)
else:
index = self._fix_neg_index(index)
value = self._list[index]
del self._dict[value]
for elem in self._list[index + 1:]:
self._dict[elem] -= 1
del self._list[index]
def insert(self, index, value):
"""Insert value at index.
Args:
index (int): Index to insert value at
value: Value to insert
Raises:
ValueError: If value already in self
IndexError: If start or end are out of range
"""
if value in self:
raise ValueError
index = self._fix_neg_index(index)
self._dict[value] = index
for elem in self._list[index:]:
self._dict[elem] += 1
self._list.insert(index, value)
def append(self, value):
"""Append value to the end.
Args:
value: Value to append
Raises:
ValueError: If value alread in self
TypeError: If value isn't hashable
"""
self._append(value)
def extend(self, values):
"""Append all values to the end.
If any of the values are present, ValueError will
be raised and none of the values will be appended.
Args:
values (Iterable): Values to append
Raises:
ValueError: If any values are already present or there are duplicates
in the passed values.
TypeError: If any of the values aren't hashable.
"""
self._extend(values)
def __iadd__(self, values):
"""Add all values to the end of self.
Args:
values (Iterable): Values to append
Raises:
ValueError: If any values are already present
"""
self._check_type(values, '+=')
self.extend(values)
return self
def remove(self, value):
"""Remove value from self.
Args:
value: Element to remove from self
Raises:
ValueError: if element is already present
"""
try:
index = self._dict[value]
except KeyError:
raise ValueError('Value "%s" is not present.')
else:
del self[index]
def remove_all(self, elems_to_delete):
"""Remove all elements from elems_to_delete, raises ValueErrors.
See Also:
discard_all
Args:
elems_to_delete (Iterable): Elements to remove.
Raises:
ValueError: If the count of any element is greater in
elems_to_delete than self.
TypeError: If any of the values aren't hashable.
"""
self._delete_all(elems_to_delete, raise_errors=True)
# Implement MutableSet
def add(self, item):
"""Add an item.
Note:
This does not raise a ValueError for an already present value like
append does. This is to match the behavior of set.add
Args:
item: Item to add
Raises:
TypeError: If item isn't hashable.
"""
self._add(item)
def update(self, values):
"""Add all values to the end.
If any of the values are present, silently ignore
them (as opposed to extend which raises an Error).
See also:
extend
Args:
values (Iterable): Values to add
Raises:
TypeError: If any of the values are unhashable.
"""
self._update(values)
def discard_all(self, elems_to_delete):
"""Discard all the elements from elems_to_delete.
This is much faster than removing them one by one.
This runs in O(len(self) + len(elems_to_delete))
Args:
elems_to_delete (Iterable): Elements to discard.
Raises:
TypeError: If any of the values aren't hashable.
"""
self._delete_all(elems_to_delete, raise_errors=False)
def discard(self, value):
"""Discard an item.
Note:
This does not raise a ValueError for a missing value like remove does.
This is to match the behavior of set.discard
"""
try:
self.remove(value)
except ValueError:
pass
def difference_update(self, other):
"""Update self to include only the differene with other."""
other = set(other)
indices_to_delete = set()
for i, elem in enumerate(self):
if elem in other:
indices_to_delete.add(i)
if indices_to_delete:
self._delete_values_by_index(indices_to_delete)
def intersection_update(self, other):
"""Update self to include only the intersection with other."""
other = set(other)
indices_to_delete = set()
for i, elem in enumerate(self):
if elem not in other:
indices_to_delete.add(i)
if indices_to_delete:
self._delete_values_by_index(indices_to_delete)
def symmetric_difference_update(self, other):
"""Update self to include only the symmetric difference with other."""
other = setlist(other)
indices_to_delete = set()
for i, item in enumerate(self):
if item in other:
indices_to_delete.add(i)
for item in other:
self.add(item)
self._delete_values_by_index(indices_to_delete)
def __isub__(self, other):
self._check_type(other, '-=')
self.difference_update(other)
return self
def __iand__(self, other):
self._check_type(other, '&=')
self.intersection_update(other)
return self
def __ior__(self, other):
self._check_type(other, '|=')
self.update(other)
return self
def __ixor__(self, other):
self._check_type(other, '^=')
self.symmetric_difference_update(other)
return self
# New methods
def shuffle(self, random=None):
"""Shuffle all of the elements in self randomly."""
random_.shuffle(self._list, random=random)
for i, elem in enumerate(self._list):
self._dict[elem] = i
def sort(self, *args, **kwargs):
"""Sort this setlist in place."""
self._list.sort(*args, **kwargs)
for index, value in enumerate(self._list):
self._dict[value] = index
class frozensetlist(_basesetlist, Hashable):
"""An immutable (hashable) setlist."""
def __hash__(self):
if not hasattr(self, '_hash_value'):
self._hash_value = _util.hash_iterable(self)
return self._hash_value
+28
View File
@@ -0,0 +1,28 @@
version: '{build}'
pull_requests:
do_not_increment_build_number: true
environment:
ProjectVersion: build$(APPVEYOR_BUILD_VERSION)
matrix:
- PYTHON: C:\PYTHON36
install:
- ps: 'if(Test-Path env:APPVEYOR_REPO_TAG_NAME) {$env:ProjectVersion=$env:APPVEYOR_REPO_TAG_NAME}'
- '%PYTHON%\python.exe --version'
- '%PYTHON%\Scripts\pip install pyinstaller'
- '%PYTHON%\Scripts\pip install markdown'
- '%PYTHON%\python.exe -m markdown README.md > README.html'
- '%PYTHON%\Scripts\pyinstaller bundle\EntranceRandomizer.spec'
- 'mkdir dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\*.pyd dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\tcl*.dll dist\EntranceRandomizer\ext'
- 'move dist\EntranceRandomizer\tk*.dll dist\EntranceRandomizer\ext'
- ps: '$env:ER_Version= &"$env:PYTHON\python.exe" -c "import Main; import re; print(re.match(''[0-9]+\\.[0-9]+\\.[0-9]+'',Main.__version__).group(0))"'
- '"%WIX%\bin\heat.exe" dir "dist\EntranceRandomizer" -sfrag -srd -suid -dr INSTALLDIR -cg ERFiles -ag -template fragment -t bundle\components.xslt -out build\components.wxs'
- '"%WIX%\bin\candle.exe" -out build\ bundle\*.wxs build\*.wxs'
- '"%WIX%\bin\light.exe" -ext WixUIExtension build\*.wixobj -o dist\EntranceRandomizer-Installer-%ProjectVersion%-win32.msi -b dist\EntranceRandomizer'
build: off
artifacts:
- path: dist/EntranceRandomizer*.msi
name: EntranceRandomizer-Installer-$(ProjectVersion)-win32.msi
- path: dist/EntranceRandomizer/
name: EntranceRandomizer-Raw-$(ProjectVersion)-win32.zip
-100
View File
@@ -1,100 +0,0 @@
---
layout: default
title: Blog
nav_order: 6
has_children: false
---
# Development Blog
{: .no_toc }
Latest updates, release notes, and development insights for ALttP Door Randomizer.
{: .fs-6 .fw-300 }
---
## Recent Posts
{% assign posts = site.posts | sort: 'date' | reverse %}
{% for post in posts limit:10 %}
<div class="post-entry" style="margin-bottom: 2em; padding-bottom: 1em; border-bottom: 1px solid #eeebee;">
<h3 style="margin-bottom: 0.5em;">
<a href="{{ post.url | relative_url }}">{{ post.title }}</a>
</h3>
<p style="color: #5C5962; font-size: 0.9em; margin-bottom: 0.5em;">
{{ post.date | date: "%B %d, %Y" }}
{% if post.author %} • by {{ post.author }}{% endif %}
</p>
<p>{{ post.excerpt | strip_html | truncatewords: 50 }}</p>
<p><a href="{{ post.url | relative_url }}">Read more →</a></p>
</div>
{% endfor %}
---
## Categories
Browse posts by category:
- [Release Notes](#release-notes)
- [Development Updates](#development-updates)
- [Feature Highlights](#feature-highlights)
- [Technical Deep Dives](#technical-deep-dives)
### Release Notes
{% assign release_posts = site.posts | where: "category", "release" %}
{% for post in release_posts limit:5 %}
- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }}
{% endfor %}
### Development Updates
{% assign dev_posts = site.posts | where: "category", "development" %}
{% for post in dev_posts limit:5 %}
- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }}
{% endfor %}
### Feature Highlights
{% assign feature_posts = site.posts | where: "category", "features" %}
{% for post in feature_posts limit:5 %}
- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }}
{% endfor %}
### Technical Deep Dives
{% assign feature_posts = site.posts | where: "category", "features" %}
{% for post in feature_posts limit:5 %}
- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }}
{% endfor %}
---
## Subscribe
Stay up to date with Door Randomizer development:
- **Discord**: Join [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer) in `#door-rando`
---
## Archive
View all posts by year:
{% assign posts_by_year = site.posts | group_by_exp: "post", "post.date | date: '%Y'" %}
{% for year in posts_by_year %}
### {{ year.name }}
{% for post in year.items %}
- [{{ post.title }}]({{ post.url | relative_url }}) - {{ post.date | date: "%B %d, %Y" }}
{% endfor %}
{% endfor %}
---
## Contributing to the Blog
Want to write a guest post or contribute content?
Contact us on Discord to discuss your ideas!
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

+3 -66
View File
@@ -68,30 +68,10 @@ Then each player can have the entire item pool defined. The name of item should
`Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here. `Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here.
**Note**: `item_pool` is a **full replacement** of the base item pool. Every item the player will receive must be listed. If you only want to tweak the default pool, use `item_pool_adjust` instead. ##### Caveat
##### Caveat
Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon. Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon.
### item_pool_adjust
This must be defined by player. Each player number should be listed with the appropriate adjustments.
Unlike `item_pool`, this section **adjusts** the base item pool generated by the randomizer settings rather than replacing it. Use positive values to add items and negative values to remove items.
```yaml
item_pool_adjust:
1:
Bottle (Random): 2 # add 2 extra random bottles
Rupees (300): 1 # add 1 extra rupee pack
Boss Heart Container: -3 # remove 3 heart containers from the base pool
```
`Bottle (Random)` follows the same bottle randomization rules as in `item_pool`. When removing bottles with `Bottle (Random): -N`, any bottle (regardless of contents) will be removed.
Removals that exceed the number of that item currently in the pool are silently ignored. Item pool adjustments are applied after beemizer but before the standard-mode Link's Uncle weapon selection, so weapons added here are eligible for that placement.
### goals ### goals
This must be defined by player. Each player number should be listed with the appropriate custom goals. This section has four primary subsections for each of the current supported events: This must be defined by player. Each player number should be listed with the appropriate custom goals. This section has four primary subsections for each of the current supported events:
@@ -254,7 +234,7 @@ You may define a list of items and a list of locations. Those items will be cons
### ow-edges ### ow-edges
This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_layout` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`. This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have either `ow_shuffle` or `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has two primary subsections: `two-way` and `groups`.
#### two-way #### two-way
@@ -273,49 +253,6 @@ someDescription:
- Stone Bridge WS* - Stone Bridge WS*
``` ```
### ow-grid
`grid` contains additional options that only have an effect when `ow_layout` is set to `grid`.
#### fixed_arrangements
Use this to dictate the relative positioning between multiple screens (or quadrants of large screens). Screens and quadrants are addressed by their OW Slot ID (independently of their world), ranging from 0x00 to 0x3F. An `arrangement` is given as a list of rows with equal lenghts. If you do not want to specify a full rectangle of screens, you can use `.` as a placeholder to allow the generator to place any screen there. The `world` property can be set to `light`, `dark` or `both` (default value) and determines for which worlds the arrangement applies.
This example forces Death Mountain to stay connected the same as vanilla in both worlds:
```
fixed_arrangements:
- arrangement:
- 0x03 0x04 0x05 0x06 0x07
- 0x0B 0x0C 0x0D 0x0E .
world: both
```
#### restricted_positions
Use this to restrict cells to a specified set of possible positions. The `world` property can be set to `light`, `dark` or `both` (default value) and determines for which worlds the restriction applies.
This example forces the Sanctuary and Link's House screens in both worlds to get placed in corners of the grid:
```
restricted_positions:
- cells:
- 0x13
- 0x2C
positions:
- 0x00
- 0x07
- 0x38
- 0x3F
world: both
```
#### wrap_horizontal / wrap_vertical
Set these to `true` to allow for overworld edge transitions to wrap from one side of a world to the opposite side. With `wrap_horizontal`, there can be east transitions on the eastern edge of the world map that send the player to the western edge of the world. With `wrap_vertical`, there can be south transitions on the southern edge of the world map that send the player to the northern edge of the world.
#### split_large_screens
When set to `true`, the four quadrants of each large screen are placed on the grid independently of each other.
### ow-crossed ### ow-crossed
This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also
+1 -10
View File
@@ -20,8 +20,7 @@ settings:
shuffle_followers: true shuffle_followers: true
shuffle: crossed shuffle: crossed
shufflelinks: true shufflelinks: true
ow_layout: wild ow_shuffle: parallel
ow_parallel: true
ow_terrain: true ow_terrain: true
ow_crossed: grouped ow_crossed: grouped
ow_keepsimilar: true ow_keepsimilar: true
@@ -67,14 +66,6 @@ item_pool:
Sanctuary Heart Container: 3 Sanctuary Heart Container: 3
Shovel: 3 Shovel: 3
Single Arrow: 1 Single Arrow: 1
Green Potion: 1
Blue Potion: 1
Red Potion: 1
item_pool_adjust:
1:
Bottle (Random): 2 # add 2 extra random bottles on top of the base pool
Magic Upgrade (1/2): 1 # add 1 extra half-magic
Boss Heart Container: -3 # remove 3 heart containers from the base pool
placements: placements:
1: 1:
Palace of Darkness - Big Chest: Hammer Palace of Darkness - Big Chest: Hammer
-473
View File
@@ -1,473 +0,0 @@
---
layout: default
title: Features
nav_order: 2
---
# Features Guide
{: .no_toc }
Comprehensive documentation of all Door Randomizer features and settings.
{: .fs-6 .fw-300 }
## Table of contents
{: .no_toc .text-delta }
1. TOC
{:toc}
---
## Important Differences from Other Randomizers
Most of these apply only when door shuffle is not vanilla.
### Starting Item
You start with a **Mirror Scroll** (looks like a map), a simplified mirror that only works in dungeons, not the overworld, and can't erase blocks like the Mirror.
### Navigation Changes
- Holes in Mire Torches Top and Mire Torches Bottom fall through to rooms below (you only need fire to get the chest)
- You can Hookshot from the left Mire wooden Bridge to the right one
- In the PoD Arena, you can bonk with Boots between the two blue crystal barriers against the ladder to reach the Arena Bridge chest and door (Bomb Jump also possible but not in logic - Boots are required)
- Flooded Rooms in Swamp can be traversed backward and may be required. Flippers are needed to get out of the water
### Logic Differences
- The chest in southeast Skull Woods that is traditionally a guaranteed Small Key in ER is not guaranteed here
- Fire Rod is not in logic for dark rooms (hard enough to figure out which dark room you are in)
- The hammerjump (and some other skips) are not in logic by default (see the mixed_travel setting for details). Doing so in a crossed dungeon seed can put you into another dungeon with the wrong dungeon id
### Boss Differences
- You have to find the attic floor and bomb it open and bring the maiden to the light to fight Blind. In cross dungeon door shuffle, the attic can be in any dungeon. If you bring the maiden to the boss arena, she will hint where the cracked floor can be found
- GT Bosses do not respawn after killing them in this mode
- Enemizer change: The attic/maiden sequence is now active and required when Blind is the boss of Thieves' Town even when bosses are shuffled
### Crystal Switches
- You can hit the PoD crystal switch in the Sexy Statue room with a bomb from the balcony above without jumping down
- GT Crystal Conveyor room (it has gibdos) - You can hit the crystal switch with a bomb when the blue barrier is up from the far side so you can leave the room to the left with blue barriers down
- PoD Arena Bridge - If entering from the bridge, you can circle round and hit the switch, then fall into the hole to respawn at the bridge again with the crystal barriers different
---
## Dungeon Settings
### Door Shuffle
Controls how dungeon doors are shuffled:
- **Vanilla** - Doors are not shuffled
- **Basic** - Doors are shuffled only within a single dungeon
- **Partitioned** - Dungeons are shuffled in 3 pools: Light World, Early Dark World, Late Dark World (Late Dark are the four dungeons that require Mitts in vanilla, including Ganon's Tower)
- **Crossed** - Doors are shuffled between dungeons as well
CLI: `--doorShuffle [vanilla|basic|partitioned|crossed]`
### Intensity
Controls which types of connections are shuffled:
- **Level 1** - Normal doors and spiral staircases are shuffled
- **Level 2** - Same as Level 1 plus open edges and both types of straight staircases are shuffled
- **Level 3** - Same as Level 2 plus Dungeon Lobbies are shuffled
CLI: `--intensity [1|2|3]`
### Door Type Shuffle
Controls which types of doors can be shuffled (only active if door shuffle is not vanilla):
- **Small Key Doors, Bomb Doors, Dash Doors** - Standard shuffled doors
- **Adds Big Key Doors** - Big key doors are now shuffled in addition to those above
- **Adds Trap Doors** - All trap doors that are permanently shut in vanilla are shuffled, excluding boss trap doors
- **Increases all Door Types** - Chaos mode where each door type per dungeon is randomized between 1 less and 4 more
CLI: `--door_type_mode [original|big|all|chaos]`
### Trap Door Removal
Options for making dungeon traversal more convenient:
- **No Removal** - Does not remove any trap doors
- **Removed If Blocking Path** - Dungeon generation removes annoying trap doors if necessary (boss trap doors never shuffled)
- **Remove Boss Traps** - Boss traps are removed, including the one near Mothula
- **Remove All Annoying Traps** - Removes all trap doors that are annoying, including boss traps
CLI: `--trap_door_mode [vanilla|optional|boss|oneway]`
### Dungeon Items
#### Small Keys
- **In Dungeon** - Small keys are restricted to their own dungeon
- **Randomized** - Small keys can be found anywhere
- **Universal** - Small keys work in any dungeon, can be found anywhere, and at least one shop will sell keys (like original Legend of Zelda)
CLI: `--keyshuffle [none|wild|universal]`
All other dungeon items (maps, compasses, Big Keys) can be restricted to their own dungeon or shuffled in the general pool.
### Key Logic Algorithm
Determines how small key door logic works:
- **Partial Protection** - Assumes you always have full inventory and worst case usage. Accounts for dark room and bunny revival glitches
- **Strict** - Small key doors require all small keys to be available to be in logic
- **Dangerous** - Assumes you never use keys out of logic (not recommended)
CLI: `--key_logic [partial|strict|dangerous]`
### Decouple Doors
Similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse.
CLI: `--decoupledoors`
### Allow Self-Looping Spiral Stairs
If enabled, spiral stairs are allowed to lead to themselves.
CLI: `--door_self_loops`
### Experimental Features
You will start as a bunny if your spawn point is in the dark world.
CLI: `--experimental`
### Crossed Dungeon Specific Settings
#### Dungeon Chest Counters
- **Auto** - Picks an appropriate setting based on other settings
- **On** - Dungeon counters on HUD always displayed
- **Off** - Dungeon counters on HUD never displayed
- **On Compass Pickup** - Dungeons with a compass item will display the counter once the compass is found
CLI: `--dungeon_counters [auto|on|off|pickup]`
#### Mixed Travel
Controls Hammerjump, PoD Arena hovering, and Mire Big Key Chest bomb jump:
- **Prevent (default)** - Rails are added to prevent these tricks. Recommended for those learning crossed dungeon mode
- **Allow** - Rooms are left alone and it is up to player discretion whether to use these tricks
- **Force** - The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required
CLI: `--mixed_travel [prevent|allow|force]`
#### Standardize Palettes
- **Standardize (default)** - Rooms in the same dungeon have their palettes changed to match
- **Original** - Rooms/supertiles keep their original palettes
CLI: `--standardize_palettes`
---
## Pool Expansions
### Pottery
Controls 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
- **Cave Pots** - The pots that are not found in dungeons are in the pool (includes Spike Cave large block)
- **Cave + Keys Pots** - Both non-dungeon pots and pots that used to have keys
- **Reduced Dungeon Pots** - Cave+Keys plus roughly 25% of dungeon pots (dynamic mode with colored pots)
- **Clustered Dungeon Pots** - Like reduced but pots grouped by logical sets, roughly 50% chosen (dynamic mode)
- **Excludes Empty Pots** - All pots that had some sort of objects under them
- **Dungeon Pots** - The pots that are in dungeons
- **Lottery** - All pots and large blocks are in the pool
CLI: `--pottery [none|keys|cave|cavekeys|reduced|clustered|nonempty|dungeon|lottery]`
#### Colorize Pots
Colors the pots that have been chosen to be part of the location pool. Forced on for dynamic modes (clustered and reduced).
CLI: `--colorizepots`
### Shuffle Enemy Drops
Controls whether enemies that drop items are randomized:
- **None** - Special enemies drop keys normally
- **Keys** - Enemies that drop keys are added to the randomization pool (includes Hyrule Castle Big Key)
- **Underworld** - Enemies in the underworld are added to the randomization pool. Blue square indicates if there are any enemies on the supertile that still have an available drop
CLI: `--dropshuffle [none|keys|underworld]`
### Shopsanity
Adds 32 shop locations (9 more in retro) to the general location pool. Multi-world supported.
Shop locations include:
- Lake Hylia Cave Shop (3 items)
- Kakariko Village Shop (3 items)
- Potion Shop (3 new items)
- Paradox Cave Shop (3 items)
- Capacity Upgrade Fairy (2 items)
- Dark Lake Hylia Shop (3 items)
- Curiosity/Red Shield Shop (3 items)
- Dark Lumberjack Shop (3 items)
- Dark Potion Shop (3 items)
- Village of Outcast Hammer Peg Shop (3 items)
- Dark Death Mountain Shop (3 items)
[See README for complete pricing guide and mechanics](/README#shopsanity)
### Take Any Caves
Three options: None, Random, and Fixed. Fixed means take any caves replace specific fairy caves:
- Desert Healer Fairy
- Swamp Healer Fairy (aka Light Hype Cave)
- Dark Death Mountain Healer Fairy
- Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb)
- Bonk Fairy (Dark)
---
## Item Randomization
### New "Items"
#### Bomb Bag
Two bomb bags are added to the item pool (look like +10 Capacity upgrades). Bombs are unable to be used until one is found.
CLI: `--bombbag`
#### Pseudo Boots
Dashing is allowed without the boots item however doors and certain rocks remain unopenable until boots are found. Specific sequence breaks like hovering and water-walking are not allowed until boots are found.
CLI: `--pseudoboots`
#### Mirror Scroll
Mirror is usable inside dungeons. Locations that require the mirror are still unattainable.
CLI: `--mirrorscroll`
#### Flute Mode
- **Normal** - Need to activate it at the village statue after finding it
- **Activated** - Can use it immediately upon finding it
CLI: `--flute_mode`
#### Bow Mode
- **Progressive** - Standard progressive bows
- **Silvers separate** - One bow in the pool and silvers are a separate item
- **Retro (progressive)** - Arrows cost rupees, need to purchase single arrow item at shop
- **Retro + Silvers** - Arrows cost rupees, one bow, silvers are separate item
CLI: `--bow_mode [progressive|silvers|retro|retro_silvers]`
### Goal Options
- **Trinity** - Find one of 3 triforces to win (pedestal, Ganon, or Murahdahla with 8 of 10 pieces)
- **Ganonhunt** - Collect the requisite triforce pieces, then defeat Ganon (Aga2 not required)
- **Completionist** - Obtain every item in the game and defeat Ganon (forces 100% accessibility)
### Item Sorting (Fill Algorithms)
Controls how items are placed in the world:
#### Balanced
Most random distribution of items (recommended).
#### Vanilla Fill
Attempts to place all items in their vanilla locations when possible. If not possible, prefers "major" locations, then heart piece locations, then the rest.
#### Major Location Restriction
Attempts to place major items in major locations. Major locations are where major items are found in the vanilla game.
#### Dungeon Restriction
Attempts to place all major items in dungeons. Overflows to overworld if necessary.
#### District Restriction
The world is divided into different regions/districts. Districts are chosen at random and filled with major items. Single green rupees indicate chosen districts.
CLI: `--algorithm [balanced|vanilla_fill|major_only|dungeon_only|district]`
### Forbidden Boss Items
Restrict items that can appear on bosses:
- **None** - Boss may have any item
- **MapCompass** - Map and compass are logically required to defeat a boss (currently bugged)
- **Dungeon** - Small keys and big keys of the dungeon are not allowed on a boss
CLI: `--restrict_boss_items [none|mapcompass|dungeon]`
---
## Entrance Randomization
### New Modes
- **Lite** - Non item entrances are vanilla, dungeons/caves grouped with restrictions
- **Lean** - Same grouping as Lite but cross-world connections allowed
- **Swapped** - Entrances are swapped with each other
### Shuffle Links House
In certain ER shuffles (not dungeonssimple or dungeonsfulls), control whether Links House is shuffled or remains vanilla.
### Skull Woods Shuffle
Options to reduce annoying Skull Woods layouts:
- **Original** - Skull woods shuffles classically amongst itself unless insanity mode
- **Restricted** - Skull woods drops are vanilla, entrances stay in skull woods and are shuffled
- **Loose** - Skull woods drops are vanilla, main ER mode's pool determines handling
- **Followlinked** - Follows Linked Drops setting
### Linked Drops Override
Controls whether drops should be linked to nearby entrances:
- **Unset** - Uses the mode's default (linked for all modes except insanity)
- **Linked** - Forces drops to be linked to their entrances
- **Independent** - Decouples drops from entrances
### Overworld Map
Option to move indicators on overworld map to reference dungeon location:
- **Default** - Showing only the prize markers on vanilla dungeon locations
- **Compass** - Compass item controls whether marker is moved to dungeon locations
- **Map** - Map item shows both prize and location of the dungeon
CLI: `--overworld_map [default|compass|map]`
---
## Enemizer
Enemizer has been incorporated into the generator. See [Enemizer in DR documentation](https://docs.google.com/document/d/1iwY7Gy50DR3SsdXVaLFIbx4xRBqo9a-e1_jAl5LMCX8/edit?usp=sharing) for extensive details.
### Enemy Shuffle
Enemies are shuffled with an enemy ban list that prevents problematic placements (blocking paths, unavoidable damage, glitches).
### Enemy Damage
The shuffled setting actually shuffles the damage table unlike the original enemizer.
### Boss Shuffle: Unique
Bosses are shuffled with some exceptions:
- Trinexx is not allowed on GT basement when DR is enabled
- Harder bosses in GT basement have logic concessions for low percentage requirements
New variant: At least one boss of each type for prize bosses will be present guarding prizes.
### Enemy Health
Health is taken into account for challenge rooms if magic or ammo is required.
### Enemy Logic
Can account for logical access to challenge rooms and item/key drops when enemies are shuffled:
- **Forbid special enemies** - Special enemies disallowed from drops and challenge rooms
- **Item drops may have special enemies** - Challenge rooms won't have special enemies, but drops may
- **Allow special enemies anywhere** - Both challenge rooms and enemy drops can have special requirements
---
## Glitched Logic
CLI: `--logic [noglitches|owglitches|hybridglitches|nologic]`
### Overworld Glitches
Includes overworld teleports, clips, superbunny, mirror to access Desert Palace East Entrance, bunny pocket access, etc.
### Hybrid Major Glitches
**Not compatible with Door Shuffle**
Includes all Overworld Glitches logic plus:
- Kikiskip to access PoD without MP or DW access
- IP Lobby clip to skip fire requirement
- Various traversals between dungeons (TT ↔ Desert, Spec rock, Paradox, Mire ↔ Hera ↔ Swamp)
- Stealing SK from Mire to open SP
- Using Mire big key to open Hera doors
---
## Game Options
### MSU Resume
Turns on MSU resume support.
CLI: `--msu_resume`
### Collection Rate
Display the collection rate unless the triforce piece counter is needed.
CLI: `--collection_rate`
### Reduce Flashing
Accessibility option to reduce some flashing animations in the game.
CLI: `--reduce_flashing`
### Shuffle Sound Effects
Shuffles a large portion of the sound effects. Can be used with the adjuster.
CLI: `--shuffle_sfx`
---
## Customizer
Create custom seeds with custom dungeons and rooms.
See [Customizer documentation](Customizer.md) for details.
---
## Generation Setup & Miscellaneous
### Create BPS Patches
Create BPS patch(es) instead of generating ROM(s) for distribution.
CLI: `--bps`
### Triforce Hunt Settings
Controls for triforce piece pool:
- `--triforce_goal_min` / `--triforce_goal_max` - Pieces to collect to win
- `--triforce_pool_min` / `--triforce_pool_max` - Pieces in item pool
- `--triforce_min_difference` / `--triforce_max_difference` - Difference between pool and goal
### Seed
Set a seed number to generate. Same seed with same settings on same version will always yield identical output.
### Count
Batch generate multiple seeds with same settings. If seed number provided, it will be used to derive subsequent seeds.
-81
View File
@@ -1,81 +0,0 @@
---
layout: default
title: Home
nav_order: 1
---
# ALttP Dungeon Randomizer
**A dungeon randomizer for The Legend of Zelda: A Link to the Past**
Door Randomizer extends the standard ALttP Entrance Randomizer with advanced dungeon shuffling capabilities that can rearrange dungeon interiors at the room level, creating entirely new dungeon experiences.
[Download Latest Release](https://github.com/aerinon/ALttPDoorRandomizer/releases/latest){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
[View on GitHub](https://github.com/aerinon/ALttPDoorRandomizer){: .btn .fs-5 .mb-4 .mb-md-0 }
---
## What is Door Randomizer?
Door Randomizer takes the dungeon experience in A Link to the Past to the next level by shuffling the connections between rooms within dungeons. Instead of just shuffling dungeon entrances, Door Randomizer can:
- **Shuffle doors within dungeons** - Rooms connect in completely new ways
- **Cross dungeon boundaries** - Walk through a door in Eastern Palace and end up in Tower of Hera
- **Adjust intensity levels** - From basic door shuffling to complete dungeon lobby randomization
## Key Features
### Basic Randomizer Features
- Supports many standard ALttP Randomizer features without needing more advanced settings.
### Door Shuffle Modes
- **Vanilla** - No shuffling, classic experience
- **Basic** - Doors shuffled within each dungeon
- **Partitioned** - Shuffle in pools (Light World, Early Dark World, Late Dark World)
- **Crossed** - Full shuffle between all dungeons
### Intensity Levels
- **Level 1** - Normal doors and spiral staircases
- **Level 2** - + open edges and straight staircases
- **Level 3** - + dungeon lobbies
### Advanced Features
- **Multiple key shuffle modes** - In dungeon, randomized, or universal keys
- **Pottery shuffle** - Randomize items under pots with multiple modes
- **Shopsanity** - 32 shop locations added to item pool
- **Enemy randomization** - Built-in enemizer with logic-aware placement
- **Custom seeds** - Create your own custom dungeons and rooms
- **Fill algorithms** - Multiple item placement strategies (balanced, vanilla fill, major only, district)
## Quick Start
1. [Download the latest release](https://github.com/aerinon/ALttPDoorRandomizer/releases/latest)
2. Extract the archive for your platform (Windows, Mac, or Linux)
3. Run `DungeonRandomizer.exe` (or the appropriate platform executable)
4. Configure your settings and generate a seed
5. Apply the patch to your A Link to the Past ROM (must be JP 1.0 version)
[See detailed installation instructions →](installation.html)
## Community
Join the discussion and get help:
- **Discord**: [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer)
- `#door-rando` - General discussion and help
- `#bug-reports` - Report bugs and issues
## Learn More
- [Features Guide](features.html) - Comprehensive feature documentation
- [Installation & Usage](installation.html) - Setup and running the randomizer
- [Known Issues](known-issues.html) - Current bugs and limitations
- [Roadmap](roadmap.html) - Future development plans
- [Blog](blog.html) - Latest updates and release notes
## Credits
Based on the [ALttP Entrance Randomizer](https://github.com/KevinCathcart/ALttPEntranceRandomizer) by KevinCathcart et al.
See [alttpr.com](https://alttpr.com/) for more details on the original VT randomizer.
-369
View File
@@ -1,369 +0,0 @@
---
layout: default
title: Installation & Usage
nav_order: 3
---
# Installation & Usage
{: .no_toc }
Complete guide to installing and running ALttP Door Randomizer.
{: .fs-6 .fw-300 }
## Table of contents
{: .no_toc .text-delta }
1. TOC
{:toc}
---
## Requirements
- Python 3.10 or higher (for running from source)
- A legal copy of The Legend of Zelda: A Link to the Past (JP 1.0 version) ROM
---
## Installation Methods
### Method 1: Pre-built Releases (Recommended)
This is the easiest method for most users.
1. Visit the [Releases page](https://github.com/aerinon/ALttPDoorRandomizer/releases)
2. Download the appropriate build for your system:
- **Windows**: `ALttPDoorRandomizer-{version}-windows.zip`
- **macOS**: `ALttPDoorRandomizer-{version}-osx.tar.gz`
- **Linux**: `ALttPDoorRandomizer-{version}-linux-focal.tar.gz`
3. Extract the archive to a folder of your choice
4. Run the executable:
- **Windows**: `DungeonRandomizer.exe`
- **macOS/Linux**: `./DungeonRandomizer` or `./Gui.py`
### Method 2: Running from Source
For developers or users who want to run the latest development version.
#### Step 1: Clone the Repository
```bash
git clone https://github.com/aerinon/ALttPDoorRandomizer.git
cd ALttPDoorRandomizer
```
For the development branch:
```bash
git checkout DoorDevUnstable
```
#### Step 2: Install Python Dependencies
The project includes a script to install platform-specific dependencies:
```bash
python resources/ci/common/local_install.py
```
This will install all necessary Python packages and platform-specific dependencies.
#### Step 3: Verify Installation
Test that everything is working:
```bash
python DungeonRandomizer.py --help
```
You should see the help text with all available options.
---
## Usage
### GUI (Graphical User Interface)
The GUI provides an easy-to-use interface for generating seeds.
#### Starting the GUI
**Pre-built release:**
- **Windows**: Double-click `DungeonRandomizer.exe`
- **macOS/Linux**: Run `./DungeonRandomizer` from terminal
**From source:**
```bash
python Gui.py
```
#### Using the GUI
1. **Select your ROM**: Click "Select ROM" and choose your ALttP ROM file
2. **Configure settings**: Use the tabs to adjust:
- **Dungeon Settings**: Door shuffle mode, intensity, key logic
- **Item Settings**: Item placement, shopsanity, pottery
- **Entrance Settings**: Entrance randomization options
- **Enemy Settings**: Enemy shuffle, boss shuffle
- **Game Options**: MSU resume, collection rate, etc.
3. **Generate**: Click "Generate" to create your seed
4. **Output**: The patched ROM will be created in the same directory
### CLI (Command-Line Interface)
The CLI offers more control and is useful for batch generation or scripting.
#### Basic Usage
```bash
python DungeonRandomizer.py [options]
```
#### Common Examples
**Basic crossed dungeon shuffle:**
```bash
python DungeonRandomizer.py --doorShuffle crossed --intensity 2
```
**With key shuffle and shopsanity:**
```bash
python DungeonRandomizer.py --doorShuffle crossed --keyshuffle wild --shopsanity
```
**Pottery and enemy drops:**
```bash
python DungeonRandomizer.py --doorShuffle basic --pottery lottery --dropshuffle underworld
```
**Generate without creating ROM (testing):**
```bash
python DungeonRandomizer.py --suppress_rom --spoiler none
```
**Create BPS patch instead of ROM:**
```bash
python DungeonRandomizer.py --bps --doorShuffle crossed
```
**Batch generation:**
```bash
python DungeonRandomizer.py --doorShuffle crossed --count 10
```
#### Key CLI Arguments
**Dungeon Settings:**
- `--doorShuffle [vanilla|basic|partitioned|crossed]` - Door shuffle mode
- `--intensity [1|2|3]` - Intensity level
- `--keyshuffle [none|wild|universal]` - Key shuffle mode
- `--key_logic [partial|strict|dangerous]` - Key logic algorithm
- `--door_type_mode [original|big|all|chaos]` - Door type shuffle
- `--trap_door_mode [vanilla|optional|boss|oneway]` - Trap door removal
**Item Settings:**
- `--algorithm [balanced|vanilla_fill|major_only|dungeon_only|district]` - Item placement
- `--pottery [none|keys|cave|cavekeys|reduced|clustered|nonempty|dungeon|lottery]` - Pot shuffle
- `--dropshuffle [none|keys|underworld]` - Enemy drop shuffle
- `--shopsanity` - Enable shop randomization
- `--bombbag` - Enable bomb bag item
- `--pseudoboots` - Enable pseudo boots
**Entrance Settings:**
- `--shuffle [vanilla|simple|restricted|full|crossed|insanity|...]` - Entrance shuffle mode
- `--overworld_map [default|compass|map]` - Overworld map mode
**Enemy Settings:**
- `--enemizercli [none|basic|shuffled]` - Enemy shuffle mode
- `--enemy_damage [default|shuffled|chaos]` - Enemy damage
- `--enemy_health [default|easy|hard]` - Enemy health
- `--shufflebosses [none|basic|normal|chaos|unique]` - Boss shuffle
**Logic:**
- `--logic [noglitches|owglitches|hybridglitches|nologic]` - Logic mode
- `--mode [open|standard|inverted]` - Game mode
- `--goal [ganon|trinity|ganonhunt|completionist]` - Victory condition
**Generation:**
- `--seed SEED` - Set seed number
- `--count N` - Generate N seeds
- `--suppress_rom` - Don't create ROM (testing)
- `--bps` - Create BPS patch instead of ROM
- `--spoiler [none|spoiler|playthrough]` - Spoiler log type
**Game Options:**
- `--msu_resume` - Enable MSU resume
- `--collection_rate` - Display collection rate
- `--reduce_flashing` - Reduce flashing animations
- `--shuffle_sfx` - Shuffle sound effects
For a complete list of options:
```bash
python DungeonRandomizer.py --help
```
---
## Multiworld Setup
Door Randomizer supports multiworld seeds where multiple players play different randomized games with shared item pools.
### Installing Multiworld Dependencies
Run the same installation script to install multiworld-specific dependencies:
```bash
python resources/ci/common/local_install.py
```
### Generating Multiworld Seeds
Create YAML files for each player (see `docs/player1.yml`, `docs/player2.yml`, `docs/player3.yml` for individual examples, or `docs/multi_mystery_example.yaml` for a complete multiworld configuration).
**Example player configuration:**
```yaml
description: Player 1's World
name: Player1
doorShuffle: crossed
intensity: 2
keyshuffle: wild
shopsanity: true
```
Generate the multiworld using the multi-mystery configuration file:
```bash
python DungeonRandomizer.py --customizer docs/multi_mystery_example.yaml
```
### Running Multiworld Server
```bash
python MultiServer.py
```
It will prompt you for the multidata file created from the multiworld generation step.
### Connecting with Multiworld Client
```bash
python MultiClient.py
```
Enter the server address and your player name to connect.
---
## Testing & Verification
### Running the Test Suite
To verify your installation and test the randomizer logic:
```bash
python TestSuite.py
```
**Test with specific settings:**
```bash
python TestSuite.py --dr basic --count 10 --tense 2
```
**Run specific test modules:**
```bash
python -m pytest test/
python -m pytest test/dungeons/TestDarkPalace.py
```
---
## Troubleshooting
### Common Issues
#### "Python not found" or "python: command not found"
- **Windows**: Make sure Python is installed and added to PATH during installation
- **macOS/Linux**: Install Python 3.10+ via your package manager or from [python.org](https://www.python.org/)
#### "No module named 'yaml'" or similar import errors
Run the dependency installation script:
```bash
python resources/ci/common/local_install.py
```
#### Pre-built executable won't run on macOS
macOS may block unsigned applications. Right-click the app and select "Open" to bypass the warning.
#### Pre-built executable won't run on Linux
Ensure the file has execute permissions:
```bash
chmod +x DungeonRandomizer
```
#### GUI window is too small or cut off
Try running from the command line to see if there are any error messages. Some display scaling settings can cause issues.
#### Generated ROM doesn't work with emulator
- Ensure you're using a clean US version of ALttP ROM (Japan 1.0)
- Try a different emulator (recommended: SNES9x, RetroArch with bsnes core)
- Verify the ROM was patched successfully (check file size and modification date)
### Getting Help
If you encounter issues:
1. Check the [Known Issues](/known-issues) page
2. Search [GitHub Issues](https://github.com/aerinon/ALttPDoorRandomizer/issues)
3. Ask in the [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer):
- `#door-rando` - General help and questions
- `#bug-reports` - Report bugs and issues
When reporting issues, please include:
- Operating system and version
- Python version (`python --version`)
- Door Randomizer version or commit hash
- Full error message or description of the problem
- Settings used (CLI command or screenshot of GUI)
---
## Advanced Usage
### Custom Presets
You can create custom preset files to save your favorite settings combinations.
See the [Customizer documentation](docs/Customizer.md) for details on creating custom seeds with custom dungeons and rooms.
### Build from Source (Packaging)
To create a standalone executable from source:
See [BUILDING.md](docs/BUILDING.md) for detailed build instructions.
### Development Workflow
For contributors:
1. Fork the repository
2. Create a feature branch from `DoorDevUnstable`:
```bash
git checkout DoorDevUnstable
git checkout -b my-feature
```
3. Make your changes
4. Run tests: `python TestSuite.py`
5. Submit a pull request to `DoorDevUnstable` (not main/master)
---
## Next Steps
- Read the [Features Guide](/features) for detailed information on all settings
- Check the [Known Issues](/known-issues) page for current limitations
- See the [Roadmap](/roadmap) for upcoming features
- Join the community on [Discord](https://discordapp.com/invite/alttprandomizer)
-366
View File
@@ -1,366 +0,0 @@
---
layout: default
title: Known Issues
nav_order: 4
---
# Known Issues & Bug Tracking
{: .no_toc }
Current known issues, bugs, and limitations in ALttP Door Randomizer.
{: .fs-6 .fw-300 }
## Table of contents
{: .no_toc .text-delta }
1. TOC
{:toc}
---
## Reporting Bugs
Before reporting a bug, please:
1. Check this page to see if the issue is already known
2. Search [existing GitHub issues](https://github.com/aerinon/ALttPDoorRandomizer/issues)
3. Verify you're using the latest version
### How to Report
Report bugs in this location:
- **Discord**: [ALTTP Randomizer Discord](https://discordapp.com/invite/alttprandomizer) in `#bug-reports` or `#door-rando` channels
### What to Include
When reporting a bug, please provide:
- **Spoiler log** if you have one
- If you don't have a spoiler log, include:
- **Seed number** and **settings used**
- **Version** of Door Randomizer (from GUI or `--version`)
- **Website or Platform** (Windows, macOS, Linux) if generator error, or which website was used
- **Description** of the issue
- **Steps to reproduce** the problem
- **Screenshots or videos** (if applicable)
- Optionally, **Save state** (if gameplay issue) (and which emulator/version)
---
## Known Issues
### Critical Issues
#### Links House on Death Mountain - no logical way off the mountain
**Status**: Under investigation
Some ER generations can have Link's House be on Death Mountain and no good way off the mountain.
---
### Major Issues
#### Crossed Dungeon Generation Can Be Slow
**Status**: Performance optimization needed
Generating crossed dungeon seeds, especially with high intensity and complex settings, can take a significant amount of time (30 seconds to several minutes).
**Why**: The dungeon generation algorithm uses constraint satisfaction and local search, which can require many attempts to find valid layouts.
**Workaround**: Be patient, or reduce intensity/complexity of settings.
#### Rare Impossible Seeds (Generation Failures)
**Status**: Occasional, investigating
In rare cases, the generator may fail to place all items or create a valid dungeon layout. This usually happens with very restrictive settings combinations.
**Workaround**: Try a different seed number, or slightly adjust settings.
---
### Moderate Issues
#### Some Enemy Placements Can Be Problematic
**Status**: Ongoing ban list maintenance
Despite the enemy ban list, some enemy placements can occasionally cause issues:
- Blocking required paths
- Causing unavoidable damage
- Creating glitchy behavior
- Bush enemies not working correctly
- Wallmasters can cause issues in certain locations
**Workaround**: Report specific problematic enemy locations so they can be added to the ban list.
**Note**: Thieves are always unkillable and banned from the entire underworld.
---
### Minor Issues
#### Forbidden Boss Items "MapCompass" Mode Bugged
**Status**: Under investigation
The `mapcompass` option for `--restrict_boss_items` is currently bugged and not recommended for use.
**Workaround**: Use `dungeon` option instead, or use `none`.
#### Swamp Trench 2 Keeps Draining
**Status**: Known issue
In some configurations, Swamp Trench 2 continues to drain even after the pot is obtained.
**Workaround**: None currently. This is a known behavior issue.
#### Killing Certain Red Guards Kills Music
**Status**: Known issue
Killing certain red guards in specific locations causes the music to stop playing.
**Workaround**: None currently. This is a cosmetic issue that doesn't affect gameplay.
#### Some Lobbies Exitable During Escape (Standard Mode)
**Status**: Known issue
In standard mode, some dungeon lobbies can be exited during the escape sequence when they shouldn't be.
**Workaround**: Avoid leaving during escape if you want to maintain intended flow.
---
### Logic Issues
These are cases where the logic may not perfectly match what's actually possible in-game:
#### GT Crystal Paths Room Logic
**Status**: Under investigation
Crystal switch logic for GT Crystal Paths room may not properly account for OHKO scenarios when traversing backwards.
#### OHKO (One-Hit KO) Logic Issues
**Status**: Under investigation
In some configurations, unavoidable damage logic or OHKO scenarios may not be properly accounted for.
#### Hammerjump Logic and Mixed Travel
**Status**: Configurable behavior
Hammerjump and similar tricks (PoD Arena hovering, Mire Big Key bomb jump) can unintentionally put you in another dungeon with the wrong dungeon ID.
**Setting**: Use `--mixed_travel` to control this behavior:
- `prevent`: Adds rails to prevent tricks (default, recommended for learning)
- `allow`: Up to player discretion
- `force`: Sections forced to same dungeon
---
## Mode-Specific Issues
### Glitched Logic Modes
Several issues specific to glitched logic modes (OWG, HMG):
#### Cave State Issues
**Status**: Under investigation
Cave state in GT hearts area may not work correctly in glitched modes.
#### Rain State Issues
**Status**: Under investigation
Rain state handling may have issues in glitched logic modes.
#### Key/Pot SRAM System
**Status**: Needs restoration
Old key/pot SRAM system needs to be restored for glitched modes to work properly.
### Inverted Mode Issues
#### Dark Sanctuary Inverted Insanity
**Status**: Under investigation
Dark Sanctuary with inverted mode and insanity entrance shuffle may have double entrance issues.
#### Dark Sanctuary at Tavern Error
**Status**: In progress
When Dark Sanctuary is at Tavern location, Link's body position may be incorrect.
### Entrance Randomization Issues
#### Dungeon Boss Exits in Non-ER
**Status**: Under investigation
Dungeon boss exits may not work correctly when entrance randomization is disabled.
### Decoupled Doors Issues
#### Sanctuary Palette Detection
**Status**: Known issue
In decoupled doors mode, sanctuary palette doesn't correctly identify adjacent rooms. Uses exits instead of entrances for determination.
**Workaround**: Use coupled doors or original palette mode.
### Shopsanity Issues
#### Output YAML: Shopsanity Potions Missing
**Status**: Under investigation
When outputting to YAML format, shopsanity potions may not be correctly included in the item pool representation.
**Workaround**: Check spoiler log for actual item locations.
---
## Known Limitations & Design Decisions
These items are **not planned to be fixed** as they are either fundamental limitations, intentional design choices, or affect all ALttP randomizers.
### Game Engine Limitations
#### Compass Count Stops Working After Triforce
After obtaining the Triforce, compass counts no longer function correctly. This is true in all ALttP randomizers and is a base game limitation.
**Workaround**: Complete dungeon exploration before finishing the game if you need counts.
#### GT Bosses Don't Respawn
GT bosses don't respawn after killing them. This is intentional base game behavior.
### Door Randomizer Design Decisions
#### Hybrid Major Glitches Not Compatible with Door Shuffle
Hybrid major glitches logic is not compatible with door shuffle modes. This is a fundamental limitation due to how glitch logic interacts with shuffled dungeon interiors.
**Workaround**: Use Overworld Glitches logic instead, or disable door shuffle.
#### Fire Rod Not in Logic for Dark Rooms
Fire Rod is not in logic for dark rooms in door shuffle mode, as it's too difficult to determine which dark room you're in after doors are shuffled.
**Note**: This is different from Advanced mode in the vanilla randomizer.
#### Hammerjump Requiring Mixed Travel Settings
Hammerjump and similar tricks (PoD Arena hovering, Mire Big Key bomb jump) require `--mixed_travel` settings in crossed mode to allow unintended dungeon transitions.
#### Blind Maiden/Attic Sequence Always Required
Blind maiden/attic sequence is required even when boss is shuffled. This maintains story consistency.
#### Southeast Skull Woods Chest Not Guaranteed Key
In Entrance Randomizer, the southeast Skull Woods chest is guaranteed to be a small key. In Door Randomizer, this is not guaranteed. This is an intentional change from ER.
### Technical Limitations
#### Pottery Lottery Mode: Multiworld Item Limit
In pottery lottery mode, only 256 items for other players can be placed under pots in your world due to technical constraints in how the game tracks items.
---
## Feature Limitations
See the [Roadmap](/roadmap) for planned features.
---
## Recently Fixed Issues
See RELEASENOTES.md for the latest fixes.
And PastReleaseNotes.md for older fixes.
---
## Performance Issues
### Memory Usage
**Issue**: Generator can use significant memory for complex seeds.
**Most affected by**:
- Multiworld with many players
- Pottery lottery mode
**Workaround**: Close other applications, or increase available RAM.
---
## Emulator Compatibility
### Known Issues with Emulators
#### Older SNES9x versions
Some older versions (< 1.55) may have graphical glitches with certain custom graphics.
#### ZSNES
**Not recommended**: ZSNES is outdated and has known accuracy issues.
---
## Workarounds & Tips
### If Generation Keeps Failing
1. Switch to a different door shuffle mode
4. Disable some restrictive settings (strict key logic, dungeon item restrictions)
5. Use balanced fill algorithm instead of district restriction
### If Seed Seems Impossible
1. Check spoiler log to verify it's beatable
2. Remember new logic differences (see Features guide)
3. Check for required techniques (boots bonking, crystal switch bombs, etc.)
4. Ask for help in Discord with spoiler log
### If Performance Is Poor
1. Use pre-built executable instead of running from source
2. Close other applications
3. Update to latest version
4. Use simpler settings combinations
---
## Issue Status Tracking
For real-time status of known issues, check:
- Discord `#bug-reports` and `#door-rando` channels
---
Last updated: 2026-01-30
This page is maintained as issues are discovered and fixed. If you notice an issue not listed here, please report it!
+21
View File
@@ -0,0 +1,21 @@
Notes:
Ice Spike Stairs seems to be off and maybe Lonely Freezor
Pod bk ledge stairs
AgaTower usually doesn't have enough Key Door candidates
(Testing) Camera spots in Aga Tower: Push Statue, Lone Statue maybe?
(Testing Waterfall Now) Spiral stairs without keys look funny - change to Incognito instead of Normal
(Can't Fix yet) Key door on Layer 1 not working (Aga Tower)
(Can't Fix yet) Key door in HC Lobby E door not passable - look into it?
Pairing Notes
30-34 4f-37 #$18,-24
30-34 37-1f #$18,-24
30-34 1f-1f???
30-35 1f-1f
30-34 36-1e #$18,-24
30-34 1e-1f
inconsistencies are caused by $0492 being set wrong
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -3
View File
@@ -1,6 +1,5 @@
settings: meta:
1: players: 1
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
-140
View File
@@ -1,140 +0,0 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x02
- 0x04
- 0x06
- 0x09
- 0x0B
- 0x0D
- 0x0F
- 0x10
- 0x12
- 0x14
- 0x16
- 0x19
- 0x1B
- 0x1D
- 0x1F
- 0x20
- 0x22
- 0x24
- 0x26
- 0x29
- 0x2B
- 0x2D
- 0x2F
- 0x30
- 0x32
- 0x34
- 0x36
- 0x39
- 0x3B
- 0x3D
- 0x3F
positions:
- 0x00
- 0x02
- 0x04
- 0x06
- 0x09
- 0x0B
- 0x0D
- 0x0F
- 0x10
- 0x12
- 0x14
- 0x16
- 0x19
- 0x1B
- 0x1D
- 0x1F
- 0x20
- 0x22
- 0x24
- 0x26
- 0x29
- 0x2B
- 0x2D
- 0x2F
- 0x30
- 0x32
- 0x34
- 0x36
- 0x39
- 0x3B
- 0x3D
- 0x3F
world: both
- cells:
- 0x01
- 0x03
- 0x05
- 0x07
- 0x08
- 0x0A
- 0x0C
- 0x0E
- 0x11
- 0x13
- 0x15
- 0x17
- 0x18
- 0x1A
- 0x1C
- 0x1E
- 0x21
- 0x23
- 0x25
- 0x27
- 0x28
- 0x2A
- 0x2C
- 0x2E
- 0x31
- 0x33
- 0x35
- 0x37
- 0x38
- 0x3A
- 0x3C
- 0x3E
positions:
- 0x01
- 0x03
- 0x05
- 0x07
- 0x08
- 0x0A
- 0x0C
- 0x0E
- 0x11
- 0x13
- 0x15
- 0x17
- 0x18
- 0x1A
- 0x1C
- 0x1E
- 0x21
- 0x23
- 0x25
- 0x27
- 0x28
- 0x2A
- 0x2C
- 0x2E
- 0x31
- 0x33
- 0x35
- 0x37
- 0x38
- 0x3A
- 0x3C
- 0x3E
world: both
+1 -2
View File
@@ -1,7 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild ow_shuffle: full
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:
@@ -1,7 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild ow_shuffle: full
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:
@@ -1,7 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild ow_shuffle: full
ow_parallel: false
ow_keepsimilar: false ow_keepsimilar: false
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:
-147
View File
@@ -1,147 +0,0 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x08
- 0x09
- 0x0A
- 0x0B
- 0x10
- 0x11
- 0x12
- 0x13
- 0x18
- 0x19
- 0x1A
- 0x1B
positions:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x08
- 0x09
- 0x0A
- 0x0B
- 0x10
- 0x11
- 0x12
- 0x13
- 0x18
- 0x19
- 0x1A
- 0x1B
world: both
- cells:
- 0x20
- 0x21
- 0x22
- 0x23
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x30
- 0x31
- 0x32
- 0x33
- 0x38
- 0x39
- 0x3A
- 0x3B
positions:
- 0x20
- 0x21
- 0x22
- 0x23
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x30
- 0x31
- 0x32
- 0x33
- 0x38
- 0x39
- 0x3A
- 0x3B
world: both
- cells:
- 0x04
- 0x05
- 0x06
- 0x07
- 0x0C
- 0x0D
- 0x0E
- 0x0F
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1C
- 0x1D
- 0x1E
- 0x1F
positions:
- 0x04
- 0x05
- 0x06
- 0x07
- 0x0C
- 0x0D
- 0x0E
- 0x0F
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1C
- 0x1D
- 0x1E
- 0x1F
world: both
- cells:
- 0x24
- 0x25
- 0x26
- 0x27
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x34
- 0x35
- 0x36
- 0x37
- 0x3C
- 0x3D
- 0x3E
- 0x3F
positions:
- 0x24
- 0x25
- 0x26
- 0x27
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x34
- 0x35
- 0x36
- 0x37
- 0x3C
- 0x3D
- 0x3E
- 0x3F
world: both
split_large_screens: true
@@ -1,6 +1,5 @@
settings: settings:
1: 1:
ow_layout: wild
ow_whirlpool: false ow_whirlpool: false
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
-3
View File
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:
-147
View File
@@ -1,147 +0,0 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x04
- 0x05
- 0x06
- 0x07
- 0x08
- 0x0F
- 0x10
- 0x17
- 0x18
- 0x1F
- 0x20
- 0x27
- 0x28
- 0x2F
- 0x30
- 0x37
- 0x38
- 0x39
- 0x3A
- 0x3B
- 0x3C
- 0x3D
- 0x3E
- 0x3F
positions:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x04
- 0x05
- 0x06
- 0x07
- 0x08
- 0x0F
- 0x10
- 0x17
- 0x18
- 0x1F
- 0x20
- 0x27
- 0x28
- 0x2F
- 0x30
- 0x37
- 0x38
- 0x39
- 0x3A
- 0x3B
- 0x3C
- 0x3D
- 0x3E
- 0x3F
world: both
- cells:
- 0x09
- 0x0A
- 0x0B
- 0x0C
- 0x0D
- 0x0E
- 0x11
- 0x16
- 0x19
- 0x1E
- 0x21
- 0x26
- 0x29
- 0x2E
- 0x31
- 0x32
- 0x33
- 0x34
- 0x35
- 0x36
positions:
- 0x09
- 0x0A
- 0x0B
- 0x0C
- 0x0D
- 0x0E
- 0x11
- 0x16
- 0x19
- 0x1E
- 0x21
- 0x26
- 0x29
- 0x2E
- 0x31
- 0x32
- 0x33
- 0x34
- 0x35
- 0x36
world: both
- cells:
- 0x12
- 0x13
- 0x14
- 0x15
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x2A
- 0x2B
- 0x2C
- 0x2D
positions:
- 0x12
- 0x13
- 0x14
- 0x15
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x2A
- 0x2B
- 0x2C
- 0x2D
world: both
- cells:
- 0x1B
- 0x1C
- 0x23
- 0x24
positions:
- 0x1B
- 0x1C
- 0x23
- 0x24
world: both
split_large_screens: true
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
-20
View File
@@ -1,6 +1,5 @@
settings: settings:
1: 1:
ow_parallel: false
ow_whirlpool: false ow_whirlpool: false
ow-edges: ow-edges:
1: 1:
@@ -78,25 +77,6 @@ ow-edges:
Desert Pass EC*: Dam WC* Desert Pass EC*: Dam WC*
Desert Pass ES*: Dam WS* Desert Pass ES*: Dam WS*
Dam EC*: South Pass WC* Dam EC*: South Pass WC*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
- 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
- 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
- 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
- 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
- 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F
- 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
- 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
world: light
restricted_positions:
- cells:
- 0x00
positions:
- 0x00
world: light
ow-whirlpools: ow-whirlpools:
1: 1:
two-way: two-way:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -1,149 +0,0 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x03
- 0x05
- 0x18
- 0x1B
- 0x1E
- 0x30
- 0x35
positions:
- 0x00
- 0x03
- 0x05
- 0x18
- 0x1B
- 0x1E
- 0x30
- 0x35
world: both
- cells:
- 0x01
- 0x04
- 0x06
- 0x19
- 0x1C
- 0x1F
- 0x31
- 0x36
positions:
- 0x01
- 0x04
- 0x06
- 0x19
- 0x1C
- 0x1F
- 0x31
- 0x36
world: both
- cells:
- 0x08
- 0x0B
- 0x0D
- 0x20
- 0x23
- 0x26
- 0x38
- 0x3D
positions:
- 0x08
- 0x0B
- 0x0D
- 0x20
- 0x23
- 0x26
- 0x38
- 0x3D
world: both
- cells:
- 0x09
- 0x0C
- 0x0E
- 0x21
- 0x24
- 0x27
- 0x39
- 0x3E
positions:
- 0x09
- 0x0C
- 0x0E
- 0x21
- 0x24
- 0x27
- 0x39
- 0x3E
world: both
- cells:
- 0x02
- 0x07
- 0x0A
- 0x0F
- 0x10
- 0x11
- 0x12
- 0x13
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x32
- 0x33
- 0x34
- 0x37
- 0x3A
- 0x3B
- 0x3C
- 0x3F
positions:
- 0x02
- 0x07
- 0x0A
- 0x0F
- 0x10
- 0x11
- 0x12
- 0x13
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x32
- 0x33
- 0x34
- 0x37
- 0x3A
- 0x3B
- 0x3C
- 0x3F
world: both
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
settings: settings:
1: 1:
ow_whirlpool: false ow_whirlpool: false
-22
View File
@@ -1,6 +1,3 @@
settings:
1:
ow_parallel: false
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -72,25 +69,6 @@ ow-edges:
Swamp Nook EC*: Swamp WC* Swamp Nook EC*: Swamp WC*
Swamp Nook ES*: Swamp WS* Swamp Nook ES*: Swamp WS*
Swamp EC*: Dark South Pass WC* Swamp EC*: Dark South Pass WC*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
- 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
- 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
- 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
- 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
- 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F
- 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
- 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
world: dark
restricted_positions:
- cells:
- 0x00
positions:
- 0x00
world: dark
ow-whirlpools: ow-whirlpools:
1: 1:
two-way: two-way:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -1,6 +1,5 @@
settings: settings:
1: 1:
ow_layout: wild
ow_terrain: false ow_terrain: false
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,5 @@
settings: settings:
1: 1:
ow_layout: wild
ow_terrain: true ow_terrain: true
ow-edges: ow-edges:
1: 1:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -1,16 +0,0 @@
ow-edges:
1:
two-way:
West Death Mountain EN*: East Death Mountain WN*
West Death Mountain ES*: East Death Mountain WS*
East Death Mountain EN*: Death Mountain TR Pegs WN*
West Dark Death Mountain EN*: East Dark Death Mountain WN*
West Dark Death Mountain ES*: East Dark Death Mountain WS*
East Dark Death Mountain EN*: Turtle Rock WN*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x03 0x04 0x05 0x06 0x07
- 0x0B 0x0C 0x0D 0x0E .
world: both
-3
View File
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -1,6 +1,3 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -1,7 +0,0 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
wrap_horizontal: true
wrap_vertical: true
@@ -1,7 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild ow_shuffle: full
ow_parallel: false
ow_terrain: true ow_terrain: true
ow-edges: ow-edges:
1: 1:
-19
View File
@@ -144,25 +144,6 @@ ow-edges:
Swamp EC*: Dark South Pass WC* Swamp EC*: Dark South Pass WC*
South Pass ES*: Lake Hylia WS* South Pass ES*: Lake Hylia WS*
Dark South Pass ES*: Ice Lake WS* Dark South Pass ES*: Ice Lake WS*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
- 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
- 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
- 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
- 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
- 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F
- 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
- 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
world: both
restricted_positions:
- cells:
- 0x00
positions:
- 0x00
world: both
ow-whirlpools: ow-whirlpools:
1: 1:
two-way: two-way:
-1
View File
@@ -7,7 +7,6 @@ requires-python = ">=3.7"
dependencies = [ dependencies = [
"aenum>=3.1.16", "aenum>=3.1.16",
"aioconsole>=0.6.2", "aioconsole>=0.6.2",
"collections-extended>=2.0.2",
"colorama>=0.4.6", "colorama>=0.4.6",
"distro>=1.9.0", "distro>=1.9.0",
"fast-enum>=1.3.0", "fast-enum>=1.3.0",
-49
View File
@@ -177,22 +177,6 @@
"full" "full"
] ]
}, },
"ow_layout": {
"choices": [
"vanilla",
"grid",
"wild"
]
},
"ow_parallel": {
"action": "store_true",
"help": "suppress",
"type": "bool"
},
"ow_unparallel": {
"action": "store_true",
"dest": "ow_parallel"
},
"ow_terrain": { "ow_terrain": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
@@ -228,15 +212,6 @@
"random" "random"
] ]
}, },
"ow_fog": {
"action": "store_true",
"help": "suppress",
"type": "bool"
},
"ow_no_fog": {
"action": "store_true",
"dest": "ow_fog"
},
"shuffle": { "shuffle": {
"choices": [ "choices": [
"vanilla", "vanilla",
@@ -378,10 +353,6 @@
"none" "none"
] ]
}, },
"require_ganon_item": {
"action": "store_true",
"type": "bool"
},
"beemizer": { "beemizer": {
"choices": [ "choices": [
"4", "3", "2", "1", "0" "4", "3", "2", "1", "0"
@@ -490,15 +461,6 @@
"always" "always"
] ]
}, },
"loothud": {
"choices": [
"never",
"presence",
"value",
"dungeon_value",
"cave_value"
]
},
"showmap": { "showmap": {
"choices": [ "choices": [
"visited", "visited",
@@ -678,9 +640,6 @@
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
}, },
"extra_keys": {
"type": "int"
},
"calc_playthrough": { "calc_playthrough": {
"action": "store_false", "action": "store_false",
"type": "bool" "type": "bool"
@@ -759,14 +718,6 @@
"type": "bool" "type": "bool"
}, },
"money_balance": {}, "money_balance": {},
"patches": {
"type": "str",
"help": "suppress"
},
"orange_potion": {
"action": "store_true",
"help": "suppress"
},
"settingsonload": { "settingsonload": {
"choices": [ "choices": [
"default", "default",
+4 -12
View File
@@ -235,18 +235,14 @@
"the entrances vanilla." "the entrances vanilla."
], ],
"ow_shuffle": [ "ow_shuffle": [
"Deprecated, use ow_layout and ow_unparallel instead."
],
"ow_layout": [
"This shuffles the layout of the overworld.", "This shuffles the layout of the overworld.",
"Vanilla: All overworld transitions are connected the same", "Vanilla: All overworld transitions are connected the same",
" way they were in the base game.", " way they were in the base game.",
"Grid: OW Screens are arranged on 8x8 grids and OW Transitions", "Parallel: Overworld transitions are shuffled, but both worlds",
" work according to this arrangement.", " will have the same pattern/shape.",
"Wild: OW Transitions are shuffled with no respect to geometric coherence." "Full: Overworld transitions are shuffled, but both worlds",
" will have an independent map shape."
], ],
"ow_unparallel": [
"With OW Layout Shuffle, this no longer forces both worlds to have a matching layout." ],
"ow_terrain": [ "ow_terrain": [
"With OW Layout Shuffle, this allows land and water edges to be connected." ], "With OW Layout Shuffle, this allows land and water edges to be connected." ],
"ow_crossed": [ "ow_crossed": [
@@ -281,9 +277,6 @@
" spots from being on any adjacent screen.", " spots from being on any adjacent screen.",
"Random: New flute spots will be generated with minimal bias." "Random: New flute spots will be generated with minimal bias."
], ],
"ow_no_fog": [
"With OW Grid Layout Shuffle or Mixed, this disables the fog that prevents",
"you from seeing unvisited screens on the overworld map." ],
"door_shuffle": [ "door_shuffle": [
"Select Door Shuffling Algorithm. (default: %(default)s)", "Select Door Shuffling Algorithm. (default: %(default)s)",
"Basic: Doors are mixed within a single dungeon.", "Basic: Doors are mixed within a single dungeon.",
@@ -425,7 +418,6 @@
"AlwaysInLogic: Dark rooms are always considered to be in logic, even if the player cannot see" "AlwaysInLogic: Dark rooms are always considered to be in logic, even if the player cannot see"
], ],
"crystal_book": [ " Book can be used indoors to flip the state of colored pegs (default: %(default)s)"], "crystal_book": [ " Book can be used indoors to flip the state of colored pegs (default: %(default)s)"],
"extra_keys": [ " Percentage of extra small keys to create for each dungeon when door shuffle is enabled (default: %(default)s)"],
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
"any_enemy_logic": [ "any_enemy_logic": [
"How to handle potential traversal between dungeon in Crossed door shuffle", "How to handle potential traversal between dungeon in Crossed door shuffle",
+6 -10
View File
@@ -111,11 +111,11 @@
"randomizer.dungeon.experimental": "Enable Experimental Features", "randomizer.dungeon.experimental": "Enable Experimental Features",
"randomizer.dungeon.dungeon_counters": "Dungeon Counters", "randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
"randomizer.dungeon.dungeon_counters.default": "Auto", "randomizer.dungeon.dungeon_counters.default": "Auto",
"randomizer.dungeon.dungeon_counters.off": "Off", "randomizer.dungeon.dungeon_counters.off": "Off",
"randomizer.dungeon.dungeon_counters.on": "On", "randomizer.dungeon.dungeon_counters.on": "On",
"randomizer.dungeon.dungeon_counters.pickup": "On Item Pickup", "randomizer.dungeon.dungeon_counters.pickup": "On Compass Pickup",
"randomizer.dungeon.mixed_travel": "Mixed Dungeon Travel ", "randomizer.dungeon.mixed_travel": "Mixed Dungeon Travel ",
"randomizer.dungeon.mixed_travel.prevent": "Prevent Mixed Dungeon Travel", "randomizer.dungeon.mixed_travel.prevent": "Prevent Mixed Dungeon Travel",
@@ -157,12 +157,10 @@
"randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere", "randomizer.enemizer.enemylogic.allow_all": "Allow special enemies anywhere",
"randomizer.overworld.layout": "Layout Shuffle", "randomizer.overworld.overworldshuffle": "Layout Shuffle",
"randomizer.overworld.layout.vanilla": "Vanilla", "randomizer.overworld.overworldshuffle.vanilla": "Vanilla",
"randomizer.overworld.layout.grid": "Grid", "randomizer.overworld.overworldshuffle.parallel": "Parallel",
"randomizer.overworld.layout.wild": "Wild", "randomizer.overworld.overworldshuffle.full": "Full",
"randomizer.overworld.parallel": "Keep Worlds Parallel",
"randomizer.overworld.terrain": "Free Terrain", "randomizer.overworld.terrain": "Free Terrain",
@@ -183,8 +181,6 @@
"randomizer.overworld.overworldflute.balanced": "Balanced", "randomizer.overworld.overworldflute.balanced": "Balanced",
"randomizer.overworld.overworldflute.random": "Random", "randomizer.overworld.overworldflute.random": "Random",
"randomizer.overworld.fog": "Overworld Map Fog",
"randomizer.entrance.openpyramid": "Pre-open Pyramid Hole", "randomizer.entrance.openpyramid": "Pre-open Pyramid Hole",
"randomizer.entrance.openpyramid.auto": "Auto", "randomizer.entrance.openpyramid.auto": "Auto",
@@ -1,13 +1,13 @@
{ {
"topOverworldFrame": {}, "topOverworldFrame": {},
"leftOverworldFrame": { "leftOverworldFrame": {
"layout": { "overworldshuffle": {
"type": "selectbox", "type": "selectbox",
"default": "vanilla", "default": "vanilla",
"options": [ "options": [
"vanilla", "vanilla",
"grid", "parallel",
"wild" "full"
] ]
}, },
"crossed": { "crossed": {
@@ -18,10 +18,7 @@
"grouped", "grouped",
"polar", "polar",
"unrestricted" "unrestricted"
], ]
"config": {
"pady": [16,0]
}
}, },
"mixed": { "mixed": {
"type": "checkbox", "type": "checkbox",
@@ -48,27 +45,22 @@
"config": { "config": {
"pady": [20,0] "pady": [20,0]
} }
},
"fog": {
"type": "checkbox",
"default": true,
"config": {
"pady": [20,0]
}
} }
}, },
"rightOverworldFrame": { "rightOverworldFrame": {
"parallel": {
"type": "checkbox",
"default": true
},
"terrain": { "terrain": {
"type": "checkbox", "type": "checkbox",
"default": false "default": false,
"config": {
"pady": [3,0]
}
}, },
"keepsimilar": { "keepsimilar": {
"type": "checkbox", "type": "checkbox",
"default": false "default": false,
"config": {
"pady": [6,0]
}
} }
} }
} }
-234
View File
@@ -1,234 +0,0 @@
---
layout: default
title: Roadmap
nav_order: 5
---
# Development Roadmap
{: .no_toc }
Future plans and upcoming features for ALttP Door Randomizer.
{: .fs-6 .fw-300 }
## Table of contents
{: .no_toc .text-delta }
1. TOC
{:toc}
---
## Project Vision
Door Randomizer aims to provide the most comprehensive and flexible dungeon randomization experience for A Link to the Past while maintaining playability and logical consistency. Our focus is on:
1. **Robust Generation** - Ensuring seeds are always completable and logical
2. **Feature Richness** - Providing extensive customization options
3. **Community Engagement** - Responding to player feedback and needs
4. **Performance** - Improving generation speed and reliability
---
## Bug Fixes
Critical and important bugs that need to be addressed:
### Critical Bugs
- Dungeon Key HUD indicator issue (total is incorrect)
- Links House on DM pathing issues (See OWR solution)
### Minor Bugs
- Resolve Swamp Trench 2 draining issue (pot obtained but still drains)
- Fix OHKO logic issues - Crystal Path backwards in GT
- Fix MapCompass forbidden boss items mode (currently bugged)
- Killing Certain Red Guards Kills Music
---
## Logic & Generation Improvements
Improving dungeon generation algorithms and logic:
- Enhanced key logic algorithms (experimental mode in New Generation)
- Rework Uncle weapon/Escape small key
- Make Sanctuary heart the logical one (simplify sanctuary heart logic)
- Investigate rupee balancing issues causing failed generations
---
## New Features
New functionality and game modes, mostly planned for the NewGeneration branch:
- Portal placement flexibility (customizability and mixed dungeon pools)
- Smarter trap door placement (reduce forced mirroring)
- DR Keysanity menu improvements
- PitWarp feature
- Door Type pooling customizations
- OWR ER standardization (District ER, Links on Death Mountain issue)
- NewSwappedAlgorithm integration
- Bonk Item Pool from Overworld Randomizer (downstream integration)
- A better hint system
- Prize Shuffle from Overworld Randomizer (downstream integration)
- Map Screen from GK Randomizer (need to investigate)
- Other downstream integrations? (custom goals, follower shuffle, flute shuffle?)
---
## Quality of Life Enhancements
Improving user experience and usability:
- Preferred location groups (control item placement with warnings/failures/retries)
- Preset support
- Control output ROM name (customize generated ROM filename)
- Progress indicators for generations
- Better entrance tracking for trackers (investigate race legality)
- Super-tile split palettes (reduce hinting from palette colors)
- Turn off Bob by default for enemizer (fewer graphical glitches)
---
## Performance Improvements
Making generation faster and more efficient:
- Reduce memory footprint
---
## Customization & Content Creation
Tools and features for creating custom content:
- Re-design customizer file format (improved YAML structure)
- Presets for customizer (start from predefined base)
- Easier enemy customization (simpler interface for customizing specific enemies - better integration with customization system)
---
## Multiworld
Enhancements for multiplayer seeds:
- Multiworld documentation
- Ability to have similar randomizations across multiworlds (makes parts of it co-op)
---
## External Tools & Integration
Integration with external tools and platforms:
- Tracker integration features
- Web-based API
---
## Technical Debt & Refactoring
Behind-the-scenes code improvements:
- Modernize Rules.py (improve readability and maintainability)
- Python upgrade (3.10 deprecated in 2026)
---
## Research & Exploration
Experimental ideas that may or may not be implemented:
### Dungeon Randomization Ideas
- **Subroom randomization** - Randomize smaller rooms within a larger room (subtile vs. supertile doors)
- **Euclidean dungeon layouts** - Dungeon randomization that makes spatial sense (rooms connected geographically)
- **Swapped dungeon generation** - Create dungeons by swapping door destinations (requires new algorithms)
---
## Branch Strategy
### Current Branches
- **NewGeneration** - Active development branch for new generation system
- **NewSwappedAlgorithm** - Experimental branch for swapped item algorithm
- **DoorDevUnstable** - Main branch, maintenance mode - only bug fixes and patches (ready to be moved to official stable)
- **DoorDev** - Stale development branch (needs update)
- **Dev/Master** - Do NOT use for PRs. Used by upstream only.
### Planned Branch Changes
- Transition from DoorDevUnstable to DoorDev as main maintenance branch
- Transition NewGeneration to main development branch
---
## Completed Features
Recent accomplishments:
- ✅ Dynamic colored pots
- ✅ Free lamp cone
- ✅ Experimental key logic improvements
---
## How to Contribute
Want to help shape the future of Door Randomizer?
### For Players
- **Test seeds** - Play with different settings and report issues
- **Provide feedback** - Share what you like and what could be better
- **Suggest features** - Tell us what you want to see
### For Developers
- **Submit PRs to DoorDevUnstable**
- **Join Discord** - Discuss development in `#door-rando`
- **Write tests** - Improve test coverage
- **Improve documentation** - Help others understand the code
### For Creators
- **Custom rooms** - Design interesting custom rooms
- **Presets** - Create balanced preset configurations
- **Tutorials** - Teach others how to use Door Rando
- **Tools** - Build complementary tools (trackers, analyzers)
---
## Release Schedule
### Regular Releases
- **Unstable builds** - Automated on every push to DoorDevUnstable
- **Hotfixes** - As needed for critical bugs
### Upcoming Releases (Tentative)
- **v1.5.x** (Q2 2026) - DoorDevUnstable becomes DoorDev
- **v1.6** (Q3 2026) - Python upgrade
- **v2.0** (Q4 2026) - New Generation branch release
---
## Success Metrics
How we measure progress:
### Technical Metrics
- Generation success rate (target: >95%)
- Average generation time (target: <10s for crossed seeds)
- Memory usage (start measuring)
- Test coverage (start measuring)
---
## Feedback & Discussion
Have thoughts on the roadmap?
- **Discord**: Join `#door-rando` to discuss
---
*This roadmap is a living document and will be updated regularly as priorities shift and progress is made.*
Last updated: 2026-01-30
-2
View File
@@ -6,8 +6,6 @@ import os
class BabelFish(): class BabelFish():
def __init__(self,subpath=["resources","app","meta"],lang=None): def __init__(self,subpath=["resources","app","meta"],lang=None):
localization_string = locale.getdefaultlocale()[0] #get set localization localization_string = locale.getdefaultlocale()[0] #get set localization
if localization_string is None:
localization_string = "en"
self.locale = localization_string[:2] if lang is None else lang #let caller override localization self.locale = localization_string[:2] if lang is None else lang #let caller override localization
self.langs = ["en"] #start with English self.langs = ["en"] #start with English
if(not self.locale == "en"): #add localization if(not self.locale == "en"): #add localization
+3 -78
View File
@@ -11,7 +11,6 @@ from yaml.representer import Representer
import RaceRandom as random import RaceRandom as random
from BaseClasses import DoorType, LocationType from BaseClasses import DoorType, LocationType
from OverworldShuffle import default_flute_connections, flute_data from OverworldShuffle import default_flute_connections, flute_data
from source.overworld.FluteShuffle import default_flute_connections, flute_data
from source.tools.MysteryUtils import get_weights, roll_settings from source.tools.MysteryUtils import get_weights, roll_settings
from Utils import HexInt, hex_representer from Utils import HexInt, hex_representer
@@ -91,8 +90,7 @@ class CustomSettings(object):
args.mystery = True args.mystery = True
else: else:
settings = defaultdict(lambda: None, player_setting) settings = defaultdict(lambda: None, player_setting)
args.ow_layout[p] = get_setting(settings['ow_layout'], args.ow_layout[p]) args.ow_shuffle[p] = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
args.ow_parallel[p] = get_setting(settings['ow_parallel'], args.ow_parallel[p])
args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p]) args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p])
args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p]) args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p])
if args.ow_crossed[p] == 'chaos': if args.ow_crossed[p] == 'chaos':
@@ -103,7 +101,6 @@ class CustomSettings(object):
args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p]) args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p])
args.ow_whirlpool[p] = get_setting(settings['ow_whirlpool'], args.ow_whirlpool[p]) args.ow_whirlpool[p] = get_setting(settings['ow_whirlpool'], args.ow_whirlpool[p])
args.ow_fluteshuffle[p] = get_setting(settings['ow_fluteshuffle'], args.ow_fluteshuffle[p]) args.ow_fluteshuffle[p] = get_setting(settings['ow_fluteshuffle'], args.ow_fluteshuffle[p])
args.ow_fog[p] = get_setting(settings['ow_fog'], args.ow_fog[p])
args.shuffle_followers[p] = get_setting(settings['shuffle_followers'], args.shuffle_followers[p]) args.shuffle_followers[p] = get_setting(settings['shuffle_followers'], args.shuffle_followers[p])
args.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p]) args.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p])
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p]) args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
@@ -139,14 +136,6 @@ class CustomSettings(object):
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p] args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
args.keyshuffle[p] = 'universal' args.keyshuffle[p] = 'universal'
ow_shuffle = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
if ow_shuffle == 'parallel':
args.ow_layout = 'wild'
args.ow_parallel = True
elif ow_shuffle == 'full':
args.ow_layout = 'wild'
args.ow_parallel = False
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p]) args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'], args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p]) args.standardize_palettes[p])
@@ -247,11 +236,6 @@ class CustomSettings(object):
return self.file_source['item_pool'] return self.file_source['item_pool']
return None return None
def get_item_pool_adjust(self):
if 'item_pool_adjust' in self.file_source:
return self.file_source['item_pool_adjust']
return None
def get_placements(self): def get_placements(self):
if 'placements' in self.file_source: if 'placements' in self.file_source:
return self.file_source['placements'] return self.file_source['placements']
@@ -270,11 +254,6 @@ class CustomSettings(object):
return self.file_source['ow-edges'] return self.file_source['ow-edges']
return None return None
def get_owgrid(self):
if 'ow-grid' in self.file_source:
return self.file_source['ow-grid']
return None
def get_owcrossed(self): def get_owcrossed(self):
if 'ow-crossed' in self.file_source: if 'ow-crossed' in self.file_source:
return self.file_source['ow-crossed'] return self.file_source['ow-crossed']
@@ -378,15 +357,13 @@ class CustomSettings(object):
self.world_rep['start_inventory'] = start_inv = {} self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range: for p in self.player_range:
settings_dict[p] = {} settings_dict[p] = {}
settings_dict[p]['ow_layout'] = world.owLayout[p] settings_dict[p]['ow_shuffle'] = world.owShuffle[p]
settings_dict[p]['ow_parallel'] = world.owParallel[p]
settings_dict[p]['ow_terrain'] = world.owTerrain[p] settings_dict[p]['ow_terrain'] = world.owTerrain[p]
settings_dict[p]['ow_crossed'] = world.owCrossed[p] settings_dict[p]['ow_crossed'] = world.owCrossed[p]
settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p] settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p]
settings_dict[p]['ow_mixed'] = world.owMixed[p] settings_dict[p]['ow_mixed'] = world.owMixed[p]
settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p] settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p]
settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[p] settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[p]
settings_dict[p]['ow_fog'] = world.owFog[p]
settings_dict[p]['shuffle_followers'] = world.shuffle_followers[p] settings_dict[p]['shuffle_followers'] = world.shuffle_followers[p]
settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p] settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p]
settings_dict[p]['shuffle'] = world.shuffle[p] settings_dict[p]['shuffle'] = world.shuffle[p]
@@ -511,7 +488,6 @@ class CustomSettings(object):
self.world_rep['ow-whirlpools'] = whirlpools = {} self.world_rep['ow-whirlpools'] = whirlpools = {}
self.world_rep['ow-tileflips'] = flips = {} self.world_rep['ow-tileflips'] = flips = {}
self.world_rep['ow-flutespots'] = flute = {} self.world_rep['ow-flutespots'] = flute = {}
self.world_rep['ow-grid'] = owgrid = {}
for p in self.player_range: for p in self.player_range:
connections = edges[p] = {} connections = edges[p] = {}
connections['two-way'] = {} connections['two-way'] = {}
@@ -532,57 +508,6 @@ class CustomSettings(object):
else: else:
flute[p]['force'] = list(HexInt(id) for id in sorted(default_flute_connections)) flute[p]['force'] = list(HexInt(id) for id in sorted(default_flute_connections))
flute[p]['forbid'] = [] flute[p]['forbid'] = []
# layout grid
owgrid[p] = {}
grid = world.owgrid[p]
if grid is None:
grid = [
[[HexInt(row * 8 + col) for col in range(8)] for row in range(8)],
[[HexInt(row * 8 + col) for col in range(8)] for row in range(8)]
]
else:
grid = [
[[HexInt(cell & 0xBF) for cell in row] for row in grid[0]],
[[HexInt(cell & 0xBF) for cell in row] for row in grid[1]]
]
# Create fixed_arrangements for both worlds
owgrid[p]['fixed_arrangements'] = [
{
'arrangement': [' '.join(f'0x{cell:02X}' for cell in row) for row in grid[0]],
'world': 'light'
},
{
'arrangement': [' '.join(f'0x{cell:02X}' for cell in row) for row in grid[1]],
'world': 'dark'
}
]
# Pin top left corners to position 0x00
owgrid[p]['restricted_positions'] = [
{
'cells': [HexInt(grid[0][0][0])],
'positions': [HexInt(0x00)],
'world': 'light'
},
{
'cells': [HexInt(grid[1][0][0])],
'positions': [HexInt(0x00)],
'world': 'dark'
}
]
# Set advanced grid options
horizontal_wrap = False
vertical_wrap = False
split_large_screens = False
if world.customizer:
grid_options = world.customizer.get_owgrid()
if grid_options and p in grid_options:
grid_options = grid_options[p]
horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True
vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True
split_large_screens = 'split_large_screens' in grid_options and grid_options['split_large_screens'] == True
owgrid[p]['wrap_horizontal'] = horizontal_wrap
owgrid[p]['wrap_vertical'] = vertical_wrap
owgrid[p]['split_large_screens'] = split_large_screens
for key, data in world.spoiler.overworlds.items(): for key, data in world.spoiler.overworlds.items():
player = data['player'] if 'player' in data else 1 player = data['player'] if 'player' in data else 1
connections = edges[player] connections = edges[player]
@@ -590,7 +515,7 @@ class CustomSettings(object):
connections[sub][data['entrance']] = data['exit'] connections[sub][data['entrance']] = data['exit']
for key, data in world.spoiler.whirlpools.items(): for key, data in world.spoiler.whirlpools.items():
player = data['player'] if 'player' in data else 1 player = data['player'] if 'player' in data else 1
whirlconnects = whirlpools[player] whirlconnects = whirlconnects[player]
sub = 'two-way' if data['direction'] == 'both' else 'one-way' sub = 'two-way' if data['direction'] == 'both' else 'one-way'
whirlconnects[sub][data['entrance']] = data['exit'] whirlconnects[sub][data['entrance']] = data['exit']
+2 -4
View File
@@ -92,15 +92,13 @@ SETTINGSTOPROCESS = {
"bombbag": "bombbag" "bombbag": "bombbag"
}, },
"overworld": { "overworld": {
"layout": "ow_layout", "overworldshuffle": "ow_shuffle",
"parallel": "ow_parallel",
"terrain": "ow_terrain", "terrain": "ow_terrain",
"crossed": "ow_crossed", "crossed": "ow_crossed",
"keepsimilar": "ow_keepsimilar", "keepsimilar": "ow_keepsimilar",
"mixed": "ow_mixed", "mixed": "ow_mixed",
"whirlpool": "ow_whirlpool", "whirlpool": "ow_whirlpool",
"overworldflute": "ow_fluteshuffle", "overworldflute": "ow_fluteshuffle"
"fog": "ow_fog"
}, },
"entrance": { "entrance": {
"entranceshuffle": "shuffle", "entranceshuffle": "shuffle",
+3 -8
View File
@@ -450,7 +450,7 @@ def init_enemy_stats():
EnemySprite.Stalfos: EnemyStats(EnemySprite.Stalfos, False, True, 6, health=4, dmg=1), EnemySprite.Stalfos: EnemyStats(EnemySprite.Stalfos, False, True, 6, health=4, dmg=1),
EnemySprite.GreenZirro: EnemyStats(EnemySprite.GreenZirro, False, False, 1, health=4, dmg=5, dmask=0x80), EnemySprite.GreenZirro: EnemyStats(EnemySprite.GreenZirro, False, False, 1, health=4, dmg=5, dmask=0x80),
EnemySprite.BlueZirro: EnemyStats(EnemySprite.BlueZirro, False, False, 7, health=8, dmg=3, dmask=0x80), EnemySprite.BlueZirro: EnemyStats(EnemySprite.BlueZirro, False, False, 7, health=8, dmg=3, dmask=0x80),
EnemySprite.Pikit: EnemyStats(EnemySprite.Pikit, False, False, 2, health=12, dmg=5), EnemySprite.Pikit: EnemyStats(EnemySprite.Pikit, False, True, 2, health=12, dmg=5),
EnemySprite.OldMan: EnemyStats(EnemySprite.OldMan, True, dmg=0), EnemySprite.OldMan: EnemyStats(EnemySprite.OldMan, True, dmg=0),
EnemySprite.PipeDown: EnemyStats(EnemySprite.PipeDown, True, dmg=0), EnemySprite.PipeDown: EnemyStats(EnemySprite.PipeDown, True, dmg=0),
@@ -2116,13 +2116,8 @@ class EnemyTable:
self.room_map = defaultdict(list) self.room_map = defaultdict(list)
self.special_bitmasks = None self.special_bitmasks = None
def write_sprite_data_to_rom(self, rom, pointer_table): def write_sprite_data_to_rom(self, rom):
ow_data_end = pointer_table['ow_sprites'][0] + pointer_table['ow_sprites'][1] pointer_address = snes_to_pc(0x09D62E)
if ow_data_end > pointer_table['uw_sprites'][2]:
# moving pointer table down
pointer_table['uw_sprites'][2] = ow_data_end
rom.write_bytes(snes_to_pc(pointer_table['uw_sprites'][3]), int16_as_bytes(ow_data_end & 0xFFFF))
pointer_address = snes_to_pc(pointer_table['uw_sprites'][2])
data_pointer = snes_to_pc(0x288000) data_pointer = snes_to_pc(0x288000)
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
rom.write_bytes(data_pointer, [0x00, 0xff]) rom.write_bytes(data_pointer, [0x00, 0xff])
+14 -10
View File
@@ -183,9 +183,9 @@ def init_sprite_requirements():
SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]), SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]),
SpriteRequirement(EnemySprite.WrongPullSwitch).affix().sub_group(3, [0x52, 0x53]), SpriteRequirement(EnemySprite.WrongPullSwitch).affix().sub_group(3, [0x52, 0x53]),
SpriteRequirement(EnemySprite.Octorok).sub_group(2, [0xc, 0x18]), SpriteRequirement(EnemySprite.Octorok).aquaphobia().sub_group(2, [0xc, 0x18]),
SpriteRequirement(EnemySprite.Moldorm).exalt().sub_group(2, 0x30), SpriteRequirement(EnemySprite.Moldorm).exalt().sub_group(2, 0x30),
SpriteRequirement(EnemySprite.Octorok4Way).sub_group(2, 0xc), SpriteRequirement(EnemySprite.Octorok4Way).aquaphobia().sub_group(2, 0xc),
SpriteRequirement(EnemySprite.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11), SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11),
SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17), SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17),
@@ -197,7 +197,7 @@ def init_sprite_requirements():
.exclude(NoFlyingRooms).exclude({0x40}), # no anti-fairies in aga tower bridge room .exclude(NoFlyingRooms).exclude({0x40}), # no anti-fairies in aga tower bridge room
SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c), SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c),
SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}), SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}),
SpriteRequirement(EnemySprite.MiniMoldorm).sub_group(1, 0x1e), SpriteRequirement(EnemySprite.MiniMoldorm).aquaphobia().sub_group(1, 0x1e),
SpriteRequirement(EnemySprite.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15), SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15),
SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]), SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]),
@@ -231,12 +231,12 @@ def init_sprite_requirements():
SpriteRequirement(EnemySprite.Hoarder2).sub_group(3, 0x11).exclude({0x10c}), SpriteRequirement(EnemySprite.Hoarder2).sub_group(3, 0x11).exclude({0x10c}),
SpriteRequirement(EnemySprite.TutorialGuard).affix(), SpriteRequirement(EnemySprite.TutorialGuard).affix(),
SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f), SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f),
SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms), SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).exclude(PitRooms), SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).exclude(PitRooms),
SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).sub_group(2, 0x13), SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).sub_group(2, 0x13),
SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms), SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms),
SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]), SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
SpriteRequirement(EnemySprite.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]), SpriteRequirement(EnemySprite.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]),
SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().sub_group(1, [0xd, 0x49]), SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().sub_group(1, [0xd, 0x49]),
SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49), SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49),
@@ -335,7 +335,7 @@ def init_sprite_requirements():
SpriteRequirement(EnemySprite.Stalfos).sub_group(0, 0x1f), SpriteRequirement(EnemySprite.Stalfos).sub_group(0, 0x1f),
SpriteRequirement(EnemySprite.GreenZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.GreenZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.BlueZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms), SpriteRequirement(EnemySprite.BlueZirro).no_drop().sub_group(3, 0x1b).exclude(NoFlyingRooms),
SpriteRequirement(EnemySprite.Pikit).no_drop().sub_group(3, 0x1b), SpriteRequirement(EnemySprite.Pikit).sub_group(3, 0x1b),
SpriteRequirement(EnemySprite.CrystalMaiden).affix(), SpriteRequirement(EnemySprite.CrystalMaiden).affix(),
SpriteRequirement(EnemySprite.OldMan).affix().sub_group(2, 0x1c), SpriteRequirement(EnemySprite.OldMan).affix().sub_group(2, 0x1c),
SpriteRequirement(EnemySprite.PipeDown).affix(), SpriteRequirement(EnemySprite.PipeDown).affix(),
@@ -471,6 +471,10 @@ 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, 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), (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, required_boss_sheets = {EnemySprite.ArmosKnight: 9, EnemySprite.Lanmolas: 11, EnemySprite.Moldorm: 12,
+7 -10
View File
@@ -84,10 +84,7 @@ UwGeneralDeny:
- [ 0x0039, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 1" - [ 0x0039, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 1"
- [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper", "AntiFairyCircle"]] #"Skull Woods - Play Pen - Hardhat Beetle" - [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper", "AntiFairyCircle"]] #"Skull Woods - Play Pen - Hardhat Beetle"
- [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2" - [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2"
- [ 0x003a, 0, ["RollerVerticalUp", "RollerVerticalDown"]] - [0x003a, 1, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003a, 1, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003a, 3, ["RollerHorizontalLeft", "RollerHorizontalRight"]]
- [ 0x003a, 4, ["RollerHorizontalLeft", "RollerHorizontalRight"]]
- [ 0x003b, 1, [ "Bumper", "AntiFairyCircle" ]] - [ 0x003b, 1, [ "Bumper", "AntiFairyCircle" ]]
- [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]] - [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003c, 0, ["BigSpike"]] - [ 0x003c, 0, ["BigSpike"]]
@@ -309,7 +306,7 @@ UwGeneralDeny:
- [0x00b0, 7, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay - [0x00b0, 7, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay
- [0x00b0, 8, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay - [0x00b0, 8, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay
- [ 0x00b1, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 1" - [ 0x00b1, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 1"
- [ 0x00b1, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Bumper" ] ] #"Misery Mire - Hourglass - Spike Trap 2" - [ 0x00b1, 3, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 2"
- [0x00b1, 4, ["Bumper", "BigSpike", "AntiFairyCircle", "Statue"]] # Wizzrobe near door - [0x00b1, 4, ["Bumper", "BigSpike", "AntiFairyCircle", "Statue"]] # Wizzrobe near door
- [ 0x00b2, 1, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00b2, 1, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
- [ 0x00b2, 3, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00b2, 3, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
@@ -342,11 +339,11 @@ UwGeneralDeny:
- [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
- [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur" - [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur"
- [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4" - [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4"
- [ 0x00c6, 2, ["Bumper", "AntiFairyCircle", "BigSpike" ]] - [0x00c6, 2, ["Bumper", "AntiFairyCircle"]]
- [ 0x00c6, 3, ["Bumper", "AntiFairyCircle", "BigSpike" ]] - [0x00c6, 3, ["Bumper", "AntiFairyCircle"]]
- [ 0x00c6, 4, ["Bumper", "AntiFairyCircle", "BigSpike" ]] - [0x00c6, 4, ["Bumper", "AntiFairyCircle"]]
- [ 0x00c6, 5, ["Bumper", "AntiFairyCircle", "BigSpike" ]] - [0x00c6, 5, ["Bumper", "AntiFairyCircle"]]
- [ 0x00c6, 6, ["Bumper", "AntiFairyCircle", "BigSpike" ]] - [0x00c6, 6, ["Bumper", "AntiFairyCircle"]]
- [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
- [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1" - [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1"
- [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2" - [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2"
+3
View File
@@ -120,6 +120,9 @@ def create_item_pool_config(world):
LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] LocationGroup('bkgt').locs(mode_grouping['GT Trash'])]
for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']:
config.reserved_locations[player].add(loc_name) config.reserved_locations[player].add(loc_name)
if world.prizeshuffle[player] != 'none':
for loc_name in mode_grouping['Prizes']:
config.reserved_locations[player].add(loc_name)
elif world.algorithm == 'major_only': elif world.algorithm == 'major_only':
config.location_groups = [ config.location_groups = [
LocationGroup('MajorItems'), LocationGroup('MajorItems'),
+132 -142
View File
@@ -147,149 +147,139 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c
'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00), 'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00),
'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)} 'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)}
ow_prize_table = {'Lost Woods Gamble': (0x2A0, 0x080), ow_prize_table = {'Links House': (0x8b1, 0xb2d),
'Lost Woods Hideout Drop': (0x338, 0x218), 'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0),
'Lost Woods Hideout Stump': (0x2A0, 0x2E0), 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0),
'Lumberjack House': (0x538, 0x158), 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080),
'Lumberjack Tree Tree': (0x4A0, 0x160), 'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0),
'Lumberjack Tree Cave': (0x550, 0x004), 'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0),
'Tower of Hera': (0x900, 0x0E0), 'Agahnims Tower': (0x820, 0x5D0),
'Spectacle Rock Cave Peak': (0x758, 0x1B7), 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280),
'Spectacle Rock Cave (Bottom)': (0x6F0, 0x257), 'Skull Woods Second Section Door (East)': (0x200, 0x240),
'Spectacle Rock Cave': (0x7C8, 0x257), 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0),
'Death Mountain Return Cave (East)': (0x648, 0x278), 'Skull Woods Final Section': (0x082, 0x0b0),
'Old Man Cave (East)': (0x658, 0x387), 'Skull Woods First Section Hole (West)': (0x200, 0x2b0),
'Old Man House (Top)': (0x8C8, 0x2D8), 'Skull Woods First Section Hole (East)': (0x340, 0x2e0),
'Old Man House (Bottom)': (0x728, 0x3A7),
'Paradox Cave (Top)': (0xDE0, 0x100),
'Paradox Cave (Bottom)': (0xDC8, 0x358),
'Paradox Cave (Middle)': (0xDDF, 0x3FF),
'Fairy Ascension Cave (Top)': (0xCD8, 0x2B8),
'Fairy Ascension Cave (Bottom)': (0xC78, 0x358),
'Spiral Cave': (0xC78, 0x207),
'Spiral Cave (Bottom)': (0xBC8, 0x2F7),
'Mimic Cave': (0xD38, 0x207),
'Hookshot Fairy': (0xD38, 0x358),
'Death Mountain Return Cave (West)': (0x5B8, 0x247),
'Old Man Cave (West)': (0x5B8, 0x348),
'Waterfall of Wishing': (0xE80, 0x2C7),
'Fortune Teller (Light)': (0x308, 0x5A8),
'Bonk Rock Cave': (0x600, 0x517),
'Sanctuary': (0x748, 0x4F4),
'Sanctuary Grave': (0x860, 0x550),
'Graveyard Cave': (0x8F8, 0x4A6),
'Kings Grave': (0x978, 0x566),
'North Fairy Cave Drop': (0xA40, 0x580),
'North Fairy Cave': (0xAA8, 0x4A8),
'Potion Shop': (0xCB8, 0x598),
'Kakariko Well Drop': (0x030, 0x6C0),
'Kakariko Well Cave': (0x0D8, 0x6F7),
'Blinds Hideout': (0x1B8, 0x6F7),
'Elder House (West)': (0x258, 0x6F7),
'Elder House (East)': (0x2E8, 0x6F7),
'Snitch Lady (West)': (0x0C8, 0x7E8),
'Snitch Lady (East)': (0x338, 0x7E8),
'Chicken House': (0x188, 0x8E8),
'Sick Kids House': (0x278, 0x8E8),
'Bush Covered House': (0x338, 0x8E8),
'Light World Bomb Hut': (0x068, 0x9D7),
'Kakariko Shop': (0x1C8, 0x9D7),
'Tavern North': (0x2B8, 0x977),
'Tavern (Front)': (0x2B8, 0x9F7),
'Agahnims Tower': (0x7F0, 0x630),
'Hyrule Castle Entrance (South)': (0x7F0, 0x750),
'Hyrule Castle Entrance (West)': (0x758, 0x644),
'Hyrule Castle Entrance (East)': (0x8E8, 0x644),
'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680),
'Hyrule Castle Secret Entrance Stairs': (0x8C8, 0x718),
'Inverted Pyramid Hole': (0x7F0, 0x6B0),
'Inverted Pyramid Entrance': (0x6E8, 0x644),
'Sahasrahlas Hut': (0xCF0, 0x747),
'Eastern Palace': (0xF40, 0x680),
'Blacksmiths Hut': (0x4E8, 0x8C8),
'Bat Cave Drop': (0x538, 0x9A8),
'Bat Cave Cave': (0x510, 0x930),
'Two Brothers House (West)': (0x1B8, 0xB88),
'Two Brothers House (East)': (0x238, 0xB88),
'Library': (0x2B8, 0xAA7),
'Kakariko Gamble Game': (0x348, 0xB78),
'Bonk Fairy (Light)': (0x788, 0xA87),
'Links House': (0x8B8, 0xB58),
'Lake Hylia Fairy': (0xD58, 0xA57),
'Long Fairy Cave': (0xF68, 0xB58),
'Desert Palace Entrance (North)': (0x148, 0xC28),
'Desert Palace Entrance (West)': (0x088, 0xCF8),
'Desert Palace Entrance (East)': (0x1E8, 0xCF8),
'Desert Palace Entrance (South)': (0x138, 0xD18),
'Checkerboard Cave': (0x2E8, 0xCF7),
'Aginahs Cave': (0x388, 0xDE8),
'Cave 45': (0x448, 0xD86),
'Light Hype Fairy': (0x988, 0xCF8),
'Lake Hylia Fortune Teller': (0xA68, 0xCC8),
'Lake Hylia Shop': (0xBF8, 0xC66),
'Capacity Upgrade': (0xCC8, 0xDE8),
'Mini Moldorm Cave': (0xA88, 0xEF8),
'Ice Rod Cave': (0xE68, 0xC37),
'Good Bee Cave': (0xEF8, 0xC36),
'20 Rupee Cave': (0xEB8, 0xCC6),
'Desert Fairy': (0x488, 0xE76),
'50 Rupee Cave': (0x4F8, 0xF47),
'Dam': (0x778, 0xF68),
'Skull Woods Final Section': (0x078, 0x0A8),
'Skull Woods Second Section Door (West)': (0x0E0, 0x1C0),
'Skull Woods Second Section Hole': (0x150, 0x120),
'Skull Woods Second Section Door (East)': (0x208, 0x208),
'Skull Woods First Section Door': (0x2C8, 0x2A8),
'Skull Woods First Section Hole (North)': (0x320, 0x1e0), 'Skull Woods First Section Hole (North)': (0x320, 0x1e0),
'Skull Woods First Section Hole (West)': (0x240, 0x380), 'Skull Woods Second Section Hole': (0x0f0, 0x0b0),
'Skull Woods First Section Hole (East)': (0x340, 0x380), 'Ice Palace': (0xca0, 0xda0),
'Dark Lumberjack Shop': (0x538, 0x177), 'Misery Mire': (0x100, 0xca0),
'Ganons Tower': (0x900, 0x0E0), 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0),
'Dark Death Mountain Fairy': (0x658, 0x387), 'Turtle Rock': (0xf11, 0x103),
'Spike Cave': (0x9F8, 0x268), 'Dark Death Mountain Ledge (West)': (0xb80, 0x180),
'Hookshot Cave Back Entrance': (0xD38, 0x038), 'Dark Death Mountain Ledge (East)': (0xc80, 0x180),
'Hookshot Cave': (0xD48, 0x107), 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240),
'Superbunny Cave (Top)': (0xDE0, 0x100), 'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680),
'Superbunny Cave (Bottom)': (0xD38, 0x358), 'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700),
'Dark Death Mountain Ledge (West)': (0xC78, 0x207), 'Kakariko Well Drop': (0x030, 0x680),
'Dark Death Mountain Ledge (East)': (0xD38, 0x207), 'Kakariko Well Cave': (0x060, 0x680),
'Turtle Rock Isolated Ledge Entrance': (0xCD8, 0x2B8), 'Bat Cave Drop': (0x520, 0x8f0),
'Dark Death Mountain Shop': (0xDC8, 0x358), 'Bat Cave Cave': (0x560, 0x940),
'Turtle Rock': (0xF48, 0x108), 'Elder House (East)': (0x2b0, 0x6a0),
'Bumper Cave (Top)': (0x5B8, 0x247), 'Elder House (West)': (0x230, 0x6a0),
'Bumper Cave (Bottom)': (0x5B8, 0x348), 'North Fairy Cave Drop': (0xa40, 0x500),
'Fortune Teller (Dark)': (0x308, 0x5A8), 'North Fairy Cave': (0xa80, 0x440),
'Dark Sanctuary Hint': (0x748, 0x4F4), 'Lost Woods Hideout Drop': (0x290, 0x200),
'Dark Potion Shop': (0xCB8, 0x598), 'Lost Woods Hideout Stump': (0x240, 0x280),
'Chest Game': (0x0C8, 0x7E8), 'Lumberjack Tree Tree': (0x4e0, 0x140),
'Thieves Town': (0x1F0, 0x7E8), 'Lumberjack Tree Cave': (0x560, 0x004),
'C-Shaped House': (0x338, 0x7E8), 'Two Brothers House (East)': (0x200, 0x0b60),
'Brewery': (0x1C8, 0x9D7), 'Two Brothers House (West)': (0x180, 0x0b60),
'Dark World Shop': (0x338, 0x8E8), 'Sanctuary Grave': (0x820, 0x4c0),
'Red Shield Shop': (0x538, 0x778), 'Sanctuary': (0x720, 0x4a0),
'Pyramid Hole': (0x800, 0x680), 'Old Man Cave (West)': (0x580, 0x2c0),
'Pyramid Entrance': (0x718, 0x7A8), 'Old Man Cave (East)': (0x620, 0x2c0),
'Pyramid Fairy': (0x7B8, 0x7A8), 'Old Man House (Bottom)': (0x720, 0x320),
'Palace of Darkness': (0xF40, 0x680), 'Old Man House (Top)': (0x820, 0x220),
'Palace of Darkness Hint': (0xD70, 0x878), 'Death Mountain Return Cave (East)': (0x600, 0x220),
'Hammer Peg Cave': (0x528, 0x9B6), 'Death Mountain Return Cave (West)': (0x500, 0x1c0),
'Archery Game': (0x348, 0xB78), 'Spectacle Rock Cave Peak': (0x720, 0x0a0),
'Bonk Fairy (Dark)': (0x788, 0xA87), 'Spectacle Rock Cave': (0x790, 0x1a0),
'Big Bomb Shop': (0x8B8, 0xB58), 'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0),
'Dark Lake Hylia Fairy': (0xD58, 0xA57), 'Paradox Cave (Bottom)': (0xd80, 0x180),
'East Dark World Hint': (0xF68, 0xB58), 'Paradox Cave (Middle)': (0xd80, 0x380),
'Mire Shed': (0x0A8, 0xCC7), 'Paradox Cave (Top)': (0xd80, 0x020),
'Misery Mire': (0x148, 0xCC7), 'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0),
'Mire Fairy': (0x1E8, 0xCC7), 'Fairy Ascension Cave (Top)': (0xc00, 0x240),
'Mire Hint': (0x388, 0xDE8), 'Spiral Cave': (0xb80, 0x180),
'Hype Cave': (0x988, 0xCF8), 'Spiral Cave (Bottom)': (0xb80, 0x2c0),
'Dark Lake Hylia Shop': (0xA68, 0xCC8), 'Bumper Cave (Bottom)': (0x580, 0x2c0),
'Ice Palace': (0xCA8, 0xE28), 'Bumper Cave (Top)': (0x500, 0x1c0),
'Dark Lake Hylia Ledge Fairy': (0xE68, 0xC37), 'Superbunny Cave (Top)': (0xd80, 0x020),
'Dark Lake Hylia Ledge Hint': (0xEF8, 0xC36), 'Superbunny Cave (Bottom)': (0xd00, 0x180),
'Dark Lake Hylia Ledge Spike Cave': (0xEB8, 0xCC6), 'Hookshot Cave': (0xc80, 0x0c0),
'Swamp Palace': (0x778, 0xF68)} 'Hookshot Cave Back Entrance': (0xcf0, 0x004),
'Ganons Tower': (0x8D0, 0x080),
'Pyramid Hole': (0x820, 0x680),
'Inverted Pyramid Hole': (0x820, 0x680),
'Pyramid Entrance': (0x640, 0x7c0),
'Inverted Pyramid Entrance': (0x6C0, 0x5D0),
'Waterfall of Wishing': (0xe80, 0x280),
'Dam': (0x759, 0xED0),
'Blinds Hideout': (0x190, 0x6c0),
'Bonk Fairy (Light)': (0x740, 0xa80),
'Lake Hylia Fairy': (0xd40, 0x9f0),
'Light Hype Fairy': (0x940, 0xc80),
'Desert Fairy': (0x420, 0xe00),
'Kings Grave': (0x920, 0x520),
'Tavern North': (0x270, 0x900),
'Chicken House': (0x120, 0x880),
'Aginahs Cave': (0x2e0, 0xd00),
'Sahasrahlas Hut': (0xcf0, 0x6c0),
'Lake Hylia Shop': (0xbc0, 0xc00),
'Capacity Upgrade': (0xca0, 0xda0),
'Blacksmiths Hut': (0x4a0, 0x880),
'Sick Kids House': (0x220, 0x880),
'Lost Woods Gamble': (0x240, 0x080),
'Fortune Teller (Light)': (0x2c0, 0x4c0),
'Snitch Lady (East)': (0x310, 0x7a0),
'Snitch Lady (West)': (0x080, 0x7a0),
'Bush Covered House': (0x2e0, 0x880),
'Tavern (Front)': (0x270, 0x980),
'Light World Bomb Hut': (0x070, 0x980),
'Kakariko Shop': (0x170, 0x980),
'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430),
'Checkerboard Cave': (0x260, 0xc00),
'Mini Moldorm Cave': (0xa40, 0xe80),
'Long Fairy Cave': (0xf60, 0xb00),
'Good Bee Cave': (0xec0, 0xc00),
'20 Rupee Cave': (0xe80, 0xca0),
'50 Rupee Cave': (0x4d0, 0xed0),
'Ice Rod Cave': (0xe00, 0xc00),
'Bonk Rock Cave': (0x5f0, 0x460),
'Library': (0x270, 0xaa0),
'Potion Shop': (0xc80, 0x4c0),
'Hookshot Fairy': (0xd00, 0x180),
'Pyramid Fairy': (0x740, 0x740),
'East Dark World Hint': (0xf60, 0xb00),
'Palace of Darkness Hint': (0xd60, 0x7c0),
'Dark Lake Hylia Fairy': (0xd40, 0x9f0),
'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00),
'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0),
'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00),
'Hype Cave': (0x940, 0xc80),
'Bonk Fairy (Dark)': (0x740, 0xa80),
'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x080, 0x7a0),
'Hammer Peg Cave': (0x4c0, 0x940),
'Red Shield Shop': (0x500, 0x680),
'Dark Sanctuary Hint': (0x720, 0x4a0),
'Fortune Teller (Dark)': (0x2c0, 0x4c0),
'Dark World Shop': (0x2e0, 0x880),
'Dark Lumberjack Shop': (0x4e0, 0x0d0),
'Dark Potion Shop': (0xc80, 0x4c0),
'Archery Game': (0x2f0, 0xaf0),
'Mire Shed': (0x060, 0xc90),
'Mire Hint': (0x2e0, 0xd00),
'Mire Fairy': (0x1c0, 0xc90),
'Spike Cave': (0x860, 0x180),
'Dark Death Mountain Shop': (0xd80, 0x180),
'Dark Death Mountain Fairy': (0x620, 0x2c0),
'Mimic Cave': (0xc80, 0x180),
'Big Bomb Shop': (0x8b1, 0xb2d),
'Dark Lake Hylia Shop': (0xa40, 0xc40),
'Lumberjack House': (0x580, 0x100),
'Lake Hylia Fortune Teller': (0xa40, 0xc40),
'Kakariko Gamble Game': (0x2f0, 0xaf0)}
default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'), default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'),
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'), ('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'),

Some files were not shown because too many files have changed in this diff Show More