Compare commits
153
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe0c233a8 | ||
|
|
5f1c08920f | ||
|
|
57db996f27 | ||
|
|
5af7073ed1 | ||
|
|
33cd2d076c | ||
|
|
cd5bc9a206 | ||
|
|
78819bbd9d | ||
|
|
b2d2aae61a | ||
|
|
320e518519 | ||
|
|
c39a31b299 | ||
|
|
0ef55c5f6a | ||
|
|
fb57e9fd34 | ||
|
|
ab2e8f7818 | ||
|
|
151765d8d2 | ||
|
|
1ae9da59bb | ||
|
|
84270544bf | ||
|
|
8ae1aefcf3 | ||
|
|
821c0ee46e | ||
|
|
a946c2b3b0 | ||
|
|
ec89afaa5e | ||
|
|
d530b68e86 | ||
|
|
924bd7a616 | ||
|
|
cdfdc15219 | ||
|
|
d2a5bb56d8 | ||
|
|
e86312f145 | ||
|
|
97aceb3f43 | ||
|
|
b6f73ffda4 | ||
|
|
c4687a3be6 | ||
|
|
4d63168e9a | ||
|
|
6474975b07 | ||
|
|
4de84df7a8 | ||
|
|
b3afa57677 | ||
|
|
24ebf3037a | ||
|
|
d64a58f636 | ||
|
|
3cb1e9d77b | ||
|
|
bdf22ac3d5 | ||
|
|
f421bba9f3 | ||
|
|
ca221b9324 | ||
|
|
a663ab892d | ||
|
|
a3adb12422 | ||
|
|
3ce66a4952 | ||
|
|
0dbae8e21e | ||
|
|
3547aef480 | ||
|
|
c9ebb80f3f | ||
|
|
b45ebdc2ab | ||
|
|
45f5f3955b | ||
|
|
4da73127a2 | ||
|
|
a4d7d63235 | ||
|
|
b2b775e243 | ||
|
|
af9b61ad7d | ||
|
|
9a6d5b2112 | ||
|
|
54e4176311 | ||
|
|
6a59f58230 | ||
|
|
b8884e9010 | ||
|
|
f04ede1e21 | ||
|
|
0e432106d5 | ||
|
|
40260c4fd4 | ||
|
|
78c389e2d5 | ||
|
|
f5d547cc5e | ||
|
|
086d1bbc85 | ||
|
|
0133bd1da7 | ||
|
|
b1a71ef9b1 | ||
|
|
699d395b3d | ||
|
|
27acabadef | ||
|
|
d3f8e5fd58 | ||
|
|
87ba33852f | ||
|
|
6859f5cd24 | ||
|
|
8a7cc02282 | ||
|
|
ac17d0ead8 | ||
|
|
ad8dff3b2d | ||
|
|
f1aef298ef | ||
|
|
e27fb51dc9 | ||
|
|
bf92fe91c2 | ||
|
|
950df6b1d0 | ||
|
|
2a24048e87 | ||
|
|
afe888a87a | ||
|
|
dab247807f | ||
|
|
09254850d3 | ||
|
|
31f634396a | ||
|
|
daa54e8aad | ||
|
|
b16777ede7 | ||
|
|
fb96d04e56 | ||
|
|
54981b5c51 | ||
|
|
eda03d8657 | ||
|
|
31682fb8a2 | ||
|
|
fabad625d2 | ||
|
|
f43fb18bad | ||
|
|
ff4f546ec9 | ||
|
|
6a58773dad | ||
|
|
51b581aa8c | ||
|
|
6949545ef8 | ||
|
|
0366198d44 | ||
|
|
2be02890bd | ||
|
|
61d7940183 | ||
|
|
f539e24ddb | ||
|
|
78dd5c65fc | ||
|
|
83254f4c78 | ||
|
|
86c9f7cfb6 | ||
|
|
d576e9ee39 | ||
|
|
00ba5d89fb | ||
|
|
e01c779f3d | ||
|
|
61ea641b38 | ||
|
|
1d89564efc | ||
|
|
81a6fd387b | ||
|
|
dc1211256c | ||
|
|
32d1c2610d | ||
|
|
3185864007 | ||
|
|
56549abfbe | ||
|
|
3744b4406b | ||
|
|
cb4e8e0242 | ||
|
|
adaeccf23e | ||
|
|
a2a291b0e1 | ||
|
|
bb7c482d1d | ||
|
|
ebebfe3ec0 | ||
|
|
5a28f42454 | ||
|
|
a2b23110c4 | ||
|
|
a84b125bbd | ||
|
|
ffbff6d08d | ||
|
|
769a8e626b | ||
|
|
3fab26f423 | ||
|
|
56e4a8c59b | ||
|
|
4b7c453219 | ||
|
|
6afdd79741 | ||
|
|
d0e9fa73fa | ||
|
|
515bc0cb0b | ||
|
|
ceb189b048 | ||
|
|
5f53da73be | ||
|
|
44415b3ca5 | ||
|
|
006d25c102 | ||
|
|
7b483780db | ||
|
|
080f3b1cca | ||
|
|
bd40ddf2e6 | ||
|
|
452981ffe2 | ||
|
|
40f8507fcb | ||
|
|
43c34f4494 | ||
|
|
594bca2a1c | ||
|
|
de343ce650 | ||
|
|
24d8da3810 | ||
|
|
7b25261f67 | ||
|
|
aac7ff1791 | ||
|
|
8480353028 | ||
|
|
7d757f3e28 | ||
|
|
3c386a0c0d | ||
|
|
89b255c031 | ||
|
|
84cd129ead | ||
|
|
9d6335efd7 | ||
|
|
b3d629bc54 | ||
|
|
2f002fa921 | ||
|
|
b5ba21a039 | ||
|
|
cb21e1e4e9 | ||
|
|
bb2228d70a | ||
|
|
cfa448947d | ||
|
|
6a0aa190a9 |
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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
-2
@@ -31,8 +31,7 @@ weights/
|
||||
/QUsb2Snes/
|
||||
/output/
|
||||
/enemizer/
|
||||
|
||||
base2current.json
|
||||
visualizations/
|
||||
|
||||
resources/user/*
|
||||
!resources/user/.gitkeep
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import logging
|
||||
import textwrap
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from AdjusterMain import adjust
|
||||
from Rom import get_sprite_from_name
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
def _get_help_string(self, action):
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
try:
|
||||
import bps.apply
|
||||
@@ -8,9 +8,9 @@ try:
|
||||
except ImportError:
|
||||
raise Exception('Could not load BPS module')
|
||||
|
||||
from Utils import output_path
|
||||
from Rom import LocalRom, apply_rom_settings
|
||||
from source.tools.BPS import bps_read_vlv
|
||||
from Utils import output_path
|
||||
|
||||
|
||||
def adjust(args):
|
||||
|
||||
+113
-65
@@ -2,7 +2,7 @@ import base64
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
from collections import OrderedDict, Counter, deque, defaultdict
|
||||
from collections import Counter, OrderedDict, defaultdict, deque
|
||||
from enum import Enum, IntEnum, unique
|
||||
|
||||
try:
|
||||
@@ -10,21 +10,28 @@ try:
|
||||
except ImportError:
|
||||
from enum import IntFlag as FastEnum
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from Utils import int16_as_bytes
|
||||
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
|
||||
from RoomData import Room
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from source.dungeon.RoomObject import RoomObject
|
||||
from source.overworld.EntranceData import door_addresses
|
||||
from Tables import (
|
||||
divisor_lookup,
|
||||
multiply_lookup,
|
||||
normal_offset_table,
|
||||
spiral_offset_table,
|
||||
)
|
||||
from Utils import int16_as_bytes
|
||||
from Versions import DRVersion, GKVersion, ORVersion
|
||||
|
||||
|
||||
class World(object):
|
||||
|
||||
def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
|
||||
def __init__(self, players, owLayout, owParallel, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
|
||||
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode):
|
||||
self.players = players
|
||||
self.teams = 1
|
||||
self.owShuffle = owShuffle.copy()
|
||||
self.owLayout = owLayout.copy()
|
||||
self.owParallel = owParallel.copy()
|
||||
self.owTerrain = {}
|
||||
self.owKeepSimilar = {}
|
||||
self.owMixed = owMixed.copy()
|
||||
@@ -32,6 +39,7 @@ class World(object):
|
||||
self.owCrossed = self.owCrossed if self.owCrossed != 'polar' or self.owMixed else 'none'
|
||||
self.owWhirlpoolShuffle = {}
|
||||
self.owFluteShuffle = {}
|
||||
self.owFog = {}
|
||||
self.shuffle = shuffle.copy()
|
||||
self.doorShuffle = doorShuffle.copy()
|
||||
self.intensity = {}
|
||||
@@ -74,6 +82,8 @@ class World(object):
|
||||
self.dark_rooms = {}
|
||||
self.damage_challenge = {}
|
||||
self.shuffle_damage_table = {}
|
||||
self.bosses_ganon = {}
|
||||
self.bosshunt_include_agas = {}
|
||||
self.ganon_item = {}
|
||||
self.ganon_item_orig = {}
|
||||
self.custom = custom
|
||||
@@ -91,6 +101,9 @@ class World(object):
|
||||
self.owswaps = {}
|
||||
self.owcrossededges = {}
|
||||
self.owwhirlpools = {}
|
||||
self.owgrid = {}
|
||||
self.owlayoutmap_lw = {}
|
||||
self.owlayoutmap_dw = {}
|
||||
self.owflutespots = {}
|
||||
self.owsectors = {}
|
||||
self.allow_flip_sanc = {}
|
||||
@@ -123,6 +136,7 @@ class World(object):
|
||||
set_player_attr('owswaps', [[],[],[]])
|
||||
set_player_attr('owcrossededges', [])
|
||||
set_player_attr('owwhirlpools', [])
|
||||
set_player_attr('owgrid', None)
|
||||
set_player_attr('owsectors', None)
|
||||
set_player_attr('allow_flip_sanc', False)
|
||||
set_player_attr('remote_items', False)
|
||||
@@ -149,6 +163,9 @@ class World(object):
|
||||
set_player_attr('keyshuffle', 'none')
|
||||
set_player_attr('bigkeyshuffle', 'none')
|
||||
set_player_attr('prizeshuffle', 'none')
|
||||
set_player_attr('showloot', 'never')
|
||||
set_player_attr('loothud', 'never')
|
||||
set_player_attr('showmap', 'map')
|
||||
set_player_attr('restrict_boss_items', 'none')
|
||||
set_player_attr('bombbag', False)
|
||||
set_player_attr('flute_mode', 'normal')
|
||||
@@ -166,6 +183,8 @@ class World(object):
|
||||
set_player_attr('escape_assist', [])
|
||||
set_player_attr('crystals_needed_for_ganon', 7)
|
||||
set_player_attr('crystals_needed_for_gt', 7)
|
||||
set_player_attr('bosses_ganon', 8)
|
||||
set_player_attr('bosshunt_include_agas', False)
|
||||
set_player_attr('ganon_item', 'silver')
|
||||
set_player_attr('crystals_ganon_orig', {})
|
||||
set_player_attr('crystals_gt_orig', {})
|
||||
@@ -184,6 +203,7 @@ class World(object):
|
||||
set_player_attr('damage_challenge', 'normal')
|
||||
set_player_attr('shuffle_damage_table', 'vanilla')
|
||||
set_player_attr('crystal_book', False)
|
||||
set_player_attr('extra_keys', 0)
|
||||
set_player_attr('collection_rate', False)
|
||||
set_player_attr('colorizepots', True)
|
||||
set_player_attr('pot_pool', {})
|
||||
@@ -335,6 +355,16 @@ class World(object):
|
||||
def is_tile_lw_like(self, 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):
|
||||
return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player)
|
||||
|
||||
@@ -354,7 +384,7 @@ class World(object):
|
||||
else:
|
||||
if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district']:
|
||||
return False
|
||||
elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']:
|
||||
elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt', 'bosshunt']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -1489,7 +1519,10 @@ class CollectionState(object):
|
||||
if not item:
|
||||
return
|
||||
changed = False
|
||||
if item.name.startswith('Progressive '):
|
||||
if item.name == "Sword and Shield":
|
||||
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 self.has('Golden Sword', item.player):
|
||||
pass
|
||||
@@ -1664,8 +1697,8 @@ class Region(object):
|
||||
self.crystal_switch = False
|
||||
|
||||
def can_reach(self, state):
|
||||
from Utils import stack_size3a
|
||||
from DungeonGenerator import GenerationException
|
||||
from Utils import stack_size3a
|
||||
if stack_size3a() > self.world.players * 1000:
|
||||
raise GenerationException(f'Infinite loop detected for "{self.name}" located at \'Region.can_reach\'')
|
||||
|
||||
@@ -1735,12 +1768,12 @@ class Entrance(object):
|
||||
|
||||
def can_reach(self, state):
|
||||
# Destination Pickup OW Only No Ledges Can S&Q Allow Mirror
|
||||
multi_step_locations = { 'Pyramid Crack': ('Big Bomb', True, True, False, True),
|
||||
'Missing Smith': ('Frog', True, False, True, True),
|
||||
'Middle Aged Man': ('Dark Blacksmith Ruins', True, False, True, True),
|
||||
'Dark Palace Button':('Kiki', True, False, False, False),
|
||||
'Old Man Drop Off': ('Lost Old Man', True, False, False, False),
|
||||
'Revealing Light': ('Suspicious Maiden', False, False, False, False)
|
||||
multi_step_locations = { 'Pyramid Crack': ('Pick Up Big Bomb', True, True, False, True),
|
||||
'Missing Smith': ('Get Frog', True, False, True, True),
|
||||
'Middle Aged Man': ('Pick Up Purple Chest', True, False, True, True),
|
||||
'Dark Palace Button': ('Pick Up Kiki', True, False, False, False),
|
||||
'Old Man Drop Off': ('Escort Old Man', True, False, False, False),
|
||||
'Revealing Light': ('Maiden Rescued', False, False, False, False)
|
||||
}
|
||||
|
||||
if self.name in multi_step_locations:
|
||||
@@ -1749,7 +1782,10 @@ class Entrance(object):
|
||||
multi_step_loc = multi_step_locations[self.name]
|
||||
if world.shuffle_followers[self.player]:
|
||||
multi_step_loc = (multi_step_loc[0], self.name == 'Pyramid Crack', multi_step_loc[2], True, True)
|
||||
step_location = world.get_location(multi_step_loc[0], self.player)
|
||||
step_location = world.find_items(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 not self in state.path:
|
||||
path = state.path.get(step_location.parent_region, (step_location.parent_region.name, None))
|
||||
@@ -1947,6 +1983,7 @@ class Dungeon(object):
|
||||
self.prize = None
|
||||
self.big_key = big_key
|
||||
self.small_keys = small_keys
|
||||
self.extra_small_keys = 0
|
||||
self.dungeon_items = dungeon_items
|
||||
self.bosses = dict()
|
||||
self.player = player
|
||||
@@ -2389,12 +2426,11 @@ class OWEdge(object):
|
||||
def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
|
||||
self.player = player
|
||||
self.name = name
|
||||
self.type = DoorType.Open
|
||||
self.direction = direction
|
||||
self.terrain = terrain
|
||||
self.parallel = None
|
||||
self.specialEntrance = False
|
||||
self.specialExit = False
|
||||
self.deadEnd = False
|
||||
|
||||
# rom properties
|
||||
self.owIndex = owIndex
|
||||
@@ -2425,7 +2461,6 @@ class OWEdge(object):
|
||||
self.worldType = WorldType.Dark
|
||||
|
||||
# logical properties
|
||||
# self.connected = False # combine with Dest?
|
||||
self.dest = None
|
||||
self.dependents = []
|
||||
self.dead = False
|
||||
@@ -2442,9 +2477,6 @@ class OWEdge(object):
|
||||
def getTarget(self):
|
||||
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):
|
||||
self.midpoint = midpoint
|
||||
self.vramLoc = vram_loc
|
||||
@@ -2866,6 +2898,10 @@ class Item(object):
|
||||
def compass(self):
|
||||
return self.type == 'Compass'
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
return self.type == 'Event'
|
||||
|
||||
@property
|
||||
def dungeon(self):
|
||||
if not self.smallkey and not self.bigkey and not self.map and not self.compass:
|
||||
@@ -3072,12 +3108,9 @@ class Spoiler(object):
|
||||
self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)])
|
||||
|
||||
def parse_meta(self):
|
||||
from Main import __version__ as ERVersion
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
|
||||
self.startinventory = list(map(str, self.world.precollected_items))
|
||||
self.metadata = {'version': ERVersion,
|
||||
'versions': {'Door':ERVersion, 'Overworld':ORVersion},
|
||||
self.metadata = {'version': GKVersion,
|
||||
'versions': {'Door': DRVersion, 'Overworld': ORVersion},
|
||||
'logic': self.world.logic,
|
||||
'mode': self.world.mode,
|
||||
'bombbag': self.world.bombbag,
|
||||
@@ -3086,13 +3119,15 @@ class Spoiler(object):
|
||||
'bow_mode': self.world.bow_mode,
|
||||
'goal': self.world.goal,
|
||||
'custom_goals': self.world.custom_goals,
|
||||
'ow_shuffle': self.world.owShuffle,
|
||||
'ow_layout': self.world.owLayout,
|
||||
'ow_parallel': self.world.owParallel,
|
||||
'ow_terrain': self.world.owTerrain,
|
||||
'ow_crossed': self.world.owCrossed,
|
||||
'ow_keepsimilar': self.world.owKeepSimilar,
|
||||
'ow_mixed': self.world.owMixed,
|
||||
'ow_whirlpool': self.world.owWhirlpoolShuffle,
|
||||
'ow_fluteshuffle': self.world.owFluteShuffle,
|
||||
'ow_fog': self.world.owFog,
|
||||
'bonk_drops': self.world.shuffle_bonk_drops,
|
||||
'shuffle_followers': self.world.shuffle_followers,
|
||||
'shuffle': self.world.shuffle,
|
||||
@@ -3116,6 +3151,8 @@ class Spoiler(object):
|
||||
'beemizer': self.world.beemizer,
|
||||
'gt_crystals': self.world.crystals_needed_for_gt,
|
||||
'ganon_crystals': self.world.crystals_needed_for_ganon,
|
||||
'ganon_bosses': self.world.bosses_ganon,
|
||||
'bosshunt_include_agas': self.world.bosshunt_include_agas,
|
||||
'ganon_item': self.world.ganon_item,
|
||||
'open_pyramid': self.world.open_pyramid,
|
||||
'accessibility': self.world.accessibility,
|
||||
@@ -3126,6 +3163,9 @@ class Spoiler(object):
|
||||
'keyshuffle': self.world.keyshuffle,
|
||||
'bigkeyshuffle': self.world.bigkeyshuffle,
|
||||
'prizeshuffle': self.world.prizeshuffle,
|
||||
'showloot': self.world.showloot,
|
||||
'loothud': self.world.loothud,
|
||||
'showmap': self.world.showmap,
|
||||
'boss_shuffle': self.world.boss_shuffle,
|
||||
'enemy_shuffle': self.world.enemy_shuffle,
|
||||
'enemy_health': self.world.enemy_health,
|
||||
@@ -3145,6 +3185,7 @@ class Spoiler(object):
|
||||
'damage_challenge': self.world.damage_challenge,
|
||||
'shuffle_damage_table': self.world.shuffle_damage_table,
|
||||
'crystal_book': self.world.crystal_book,
|
||||
'extra_keys': self.world.extra_keys,
|
||||
'triforcegoal': self.world.treasure_hunt_count,
|
||||
'triforcepool': self.world.treasure_hunt_total,
|
||||
'race': self.world.settings.world_rep['meta']['race'],
|
||||
@@ -3152,6 +3193,8 @@ class Spoiler(object):
|
||||
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)},
|
||||
'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):
|
||||
from ItemList import set_default_triforce
|
||||
@@ -3290,7 +3333,7 @@ class Spoiler(object):
|
||||
self.parse_meta()
|
||||
with open(filename, 'w') as outfile:
|
||||
line_width = 35
|
||||
outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed))
|
||||
outfile.write('ALttP GwaaKiwi Randomizer - Seed: %s\n\n' % (self.world.seed))
|
||||
for k,v in self.metadata["versions"].items():
|
||||
outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v)
|
||||
for player in range(1, self.world.players + 1):
|
||||
@@ -3305,7 +3348,7 @@ class Spoiler(object):
|
||||
self.parse_meta()
|
||||
with open(filename, 'w') as outfile:
|
||||
line_width = 35
|
||||
outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed))
|
||||
outfile.write('ALttP GwaaKiwi Randomizer - Seed: %s\n\n' % (self.world.seed))
|
||||
for k,v in self.metadata["versions"].items():
|
||||
outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v)
|
||||
if self.metadata['user_notes']:
|
||||
@@ -3336,6 +3379,10 @@ class Spoiler(object):
|
||||
if custom['ganongoal'] and 'requirements' in custom['ganongoal']:
|
||||
outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n')
|
||||
outfile.write(' %s\n' % custom['ganongoal']['goaltext'])
|
||||
elif self.metadata['goal'][player] == 'bosshunt':
|
||||
outfile.write('Ganon Requirement:'.ljust(line_width) + '%s bosses%s\n' %
|
||||
(str(self.world.bosses_ganon[player]),
|
||||
' (including both Agahnims)' if self.world.bosshunt_include_agas[player] else ''))
|
||||
else:
|
||||
outfile.write('Ganon Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_ganon_orig[player]))
|
||||
if custom['pedgoal'] and 'requirements' in custom['pedgoal']:
|
||||
@@ -3364,15 +3411,18 @@ class Spoiler(object):
|
||||
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('\n')
|
||||
outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player])
|
||||
if self.metadata['ow_shuffle'][player] != 'vanilla':
|
||||
outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_layout'][player])
|
||||
if self.metadata['ow_layout'][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('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player])
|
||||
if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none':
|
||||
if self.metadata['ow_layout'][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('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('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('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player])
|
||||
if self.metadata['shuffle'][player] != 'vanilla':
|
||||
@@ -3390,6 +3440,9 @@ class Spoiler(object):
|
||||
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player])
|
||||
outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][player])
|
||||
outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player])
|
||||
outfile.write('Show Value of Checks:'.ljust(line_width) + '%s\n' % self.metadata['showloot'][player])
|
||||
outfile.write('Show 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('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player])
|
||||
outfile.write('\n')
|
||||
outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player])
|
||||
@@ -3415,6 +3468,7 @@ class Spoiler(object):
|
||||
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('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('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race']))
|
||||
|
||||
@@ -3487,40 +3541,31 @@ class Spoiler(object):
|
||||
outfile.write(f'{fairy}: {bottle}\n')
|
||||
|
||||
if self.maps:
|
||||
if 'all' in self.settings or 'flute' in self.settings:
|
||||
# flute shuffle
|
||||
def write_map(type, title):
|
||||
for player in range(1, self.world.players + 1):
|
||||
if ('flute', player) in self.maps:
|
||||
outfile.write('\n\nFlute Spots:\n\n')
|
||||
if (type, player) in self.maps:
|
||||
outfile.write('\n\n' + title + '\n\n')
|
||||
break
|
||||
for player in range(1, self.world.players + 1):
|
||||
if ('flute', player) in self.maps:
|
||||
if (type, 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'])
|
||||
outfile.write(self.maps[(type, player)]['text'])
|
||||
|
||||
if 'all' in self.settings or 'flute' in self.settings:
|
||||
# flute shuffle
|
||||
write_map('flute', 'Flute Spots:')
|
||||
|
||||
if 'all' in self.settings or 'overworld' in self.settings:
|
||||
# overworld 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'])
|
||||
write_map('swaps', 'OW Tile Flips:')
|
||||
|
||||
# crossed groups
|
||||
for player in range(1, self.world.players + 1):
|
||||
if ('groups', player) in self.maps:
|
||||
outfile.write('\n\nOW Crossed Groups:\n\n')
|
||||
break
|
||||
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'])
|
||||
write_map('groups', 'OW Crossed Groups:')
|
||||
|
||||
# grid layout
|
||||
write_map('layout_grid_lw', 'Light World Layout:')
|
||||
write_map('layout_grid_dw', 'Dark World Layout:')
|
||||
|
||||
if self.overworlds and ('all' in self.settings or 'overworld' in self.settings):
|
||||
outfile.write('\n\nOverworld Edges:\n\n')
|
||||
@@ -3743,8 +3788,9 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2}
|
||||
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
|
||||
|
||||
# byte 2: GGGD DFFH (goal, diff, item_func, hints)
|
||||
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5,
|
||||
'ganonhunt': 6, 'completionist': 7, 'sanctuary': 1}
|
||||
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3,
|
||||
'crystals': 4, 'trinity': 5, 'ganonhunt': 6, 'completionist': 7,
|
||||
'sanctuary': 1, 'bosshunt': 6}
|
||||
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||
func_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||
|
||||
@@ -3784,11 +3830,11 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
|
||||
|
||||
# byte 10: settings_version
|
||||
|
||||
# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed)
|
||||
or_mode = {"vanilla": 0, "parallel": 1, "full": 2}
|
||||
# byte 11: POOT WCCC (parallel, OWR layout, free terrain, whirlpools, OWR crossed)
|
||||
orlayout_mode = {"vanilla": 0, "grid": 1, "wild": 2}
|
||||
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4}
|
||||
|
||||
# byte 12: KMBQ FF?? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots)
|
||||
# byte 12: KMBQ FFO? (keep similar, mixed/tile flip, bonk drops, follower quests, flute spots, fog)
|
||||
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}
|
||||
|
||||
# byte 13: FBBB TTPP (flute_mode, bow_mode, take_any, prize shuffle)
|
||||
@@ -3851,12 +3897,12 @@ class Settings(object):
|
||||
|
||||
settings_version,
|
||||
|
||||
(or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0)
|
||||
(0x80 if w.owParallel[p] else 0) | (orlayout_mode[w.owLayout[p]] << 5) | (0x10 if w.owTerrain[p] else 0)
|
||||
| (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)
|
||||
| (0x20 if w.shuffle_bonk_drops[p] else 0) | (0x10 if w.shuffle_followers[p] else 0)
|
||||
| (flutespot_mode[w.owFluteShuffle[p]] << 4),
|
||||
| (flutespot_mode[w.owFluteShuffle[p]] << 4) | (0x02 if w.owFog[p] else 0),
|
||||
|
||||
(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]]),
|
||||
@@ -3933,7 +3979,8 @@ class Settings(object):
|
||||
args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
|
||||
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
|
||||
|
||||
args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5]
|
||||
args.ow_parallel[p] = True if settings[11] & 0x80 else False
|
||||
args.ow_layout[p] = r(orlayout_mode)[(settings[11] & 0x60) >> 5]
|
||||
args.ow_terrain[p] = True if settings[11] & 0x10 else False
|
||||
args.ow_whirlpool[p] = True if settings[11] & 0x08 else False
|
||||
args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)]
|
||||
@@ -3943,6 +3990,7 @@ class Settings(object):
|
||||
args.bonk_drops[p] = True if settings[12] & 0x20 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_fog[p] = True if settings[12] & 0x02 else False
|
||||
|
||||
if len(settings) > 13:
|
||||
args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import RaceRandom as random
|
||||
|
||||
import RaceRandom as random
|
||||
from BaseClasses import Boss, FillError
|
||||
from source.enemizer.Bossmizer import boss_adjust
|
||||
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
# 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
|
||||
- \~Merged in DR v1.5.2~
|
||||
- Reverted key count update
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
# 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
|
||||
@@ -2,14 +2,13 @@ import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
import shlex
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
|
||||
from Utils import update_deprecated_args
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
from Utils import update_deprecated_args
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
@@ -90,6 +89,7 @@ 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('--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('--skip_money_balance', action="store_true", help=argparse.SUPPRESS)
|
||||
|
||||
# included for backwards compatibility
|
||||
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
|
||||
@@ -107,7 +107,14 @@ def parse_cli(argv, no_defaults=False):
|
||||
ret = parser.parse_args(argv)
|
||||
|
||||
if ret.keysanity:
|
||||
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = 'wild' * 4
|
||||
if ret.mapshuffle == "none":
|
||||
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:
|
||||
ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle
|
||||
@@ -121,33 +128,65 @@ def parse_cli(argv, no_defaults=False):
|
||||
ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any
|
||||
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:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, player_num + 1):
|
||||
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
|
||||
if playerargs.filename:
|
||||
playersettings = apply_settings_file({}, playerargs.filename)
|
||||
for k, v in playersettings.items():
|
||||
setattr(playerargs, k, v)
|
||||
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle',
|
||||
'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
|
||||
'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers',
|
||||
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory',
|
||||
'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
|
||||
'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
|
||||
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern',
|
||||
'skullwoods', 'linked_drops',
|
||||
'pseudoboots', 'mirrorscroll', 'dark_rooms', 'damage_challenge', 'shuffle_damage_table', 'crystal_book', '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']:
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty',
|
||||
'item_functionality', 'ow_shuffle', 'ow_layout',
|
||||
'ow_parallel', 'ow_terrain', 'ow_crossed',
|
||||
'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool',
|
||||
'ow_fluteshuffle', 'ow_fog', 'flute_mode', 'bow_mode',
|
||||
'take_any', 'boots_hint', 'shuffle_followers',
|
||||
'shuffle', 'door_shuffle', 'intensity',
|
||||
'crystals_ganon', 'crystals_gt', 'bosses_ganon',
|
||||
'bosshunt_include_agas', 'ganon_item', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle',
|
||||
'bigkeyshuffle', 'prizeshuffle', 'showloot', 'loothud',
|
||||
'showmap', 'startinventory', 'usestartinventory',
|
||||
'bombbag', 'shuffleganon', 'overworld_map',
|
||||
'restrict_boss_items', 'triforce_max_difference',
|
||||
'triforce_pool_min', 'triforce_pool_max',
|
||||
'triforce_goal_min', 'triforce_goal_max',
|
||||
'triforce_min_difference', 'triforce_goal',
|
||||
'triforce_pool', 'shufflelinks', 'shuffletavern',
|
||||
'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)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
@@ -178,6 +217,8 @@ def parse_settings():
|
||||
"goal": "ganon",
|
||||
"crystals_gt": "7",
|
||||
"crystals_ganon": "7",
|
||||
"bosses_ganon": "8",
|
||||
"bosshunt_include_agas": False,
|
||||
"ganon_item": "silver",
|
||||
"swords": "random",
|
||||
"flute_mode": "normal",
|
||||
@@ -195,13 +236,18 @@ def parse_settings():
|
||||
# Shuffle Ganon defaults to TRUE
|
||||
"openpyramid": "auto",
|
||||
"shuffleganon": True,
|
||||
"ow_shuffle": "vanilla",
|
||||
"ow_shuffle": "vanilla", # for backwards compatibility
|
||||
"ow_layout": "vanilla",
|
||||
"ow_parallel": True,
|
||||
"ow_unparallel": False,
|
||||
"ow_terrain": False,
|
||||
"ow_crossed": "none",
|
||||
"ow_keepsimilar": False,
|
||||
"ow_mixed": False,
|
||||
"ow_whirlpool": False,
|
||||
"ow_fluteshuffle": "vanilla",
|
||||
"ow_fog": True,
|
||||
"ow_no_fog": False,
|
||||
"shuffle_followers": False,
|
||||
"bonk_drops": False,
|
||||
"shuffle": "vanilla",
|
||||
@@ -217,6 +263,7 @@ def parse_settings():
|
||||
"damage_challenge": "normal",
|
||||
"shuffle_damage_table": "vanilla",
|
||||
"crystal_book": False,
|
||||
"extra_keys": 0,
|
||||
|
||||
"shuffleenemies": "none",
|
||||
"shufflebosses": "none",
|
||||
@@ -235,6 +282,9 @@ def parse_settings():
|
||||
"keyshuffle": "none",
|
||||
"bigkeyshuffle": "none",
|
||||
"prizeshuffle": "none",
|
||||
"showloot": "never",
|
||||
"loothud": "never",
|
||||
"showmap": "map",
|
||||
"keysanity": False,
|
||||
"door_shuffle": "vanilla",
|
||||
"intensity": 3,
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import List
|
||||
|
||||
import RaceRandom as random
|
||||
|
||||
|
||||
def _load_entries():
|
||||
entries = []
|
||||
with open("data/damage_table.bin", 'rb') as stream:
|
||||
|
||||
+103
-20
@@ -1,27 +1,63 @@
|
||||
import RaceRandom as random
|
||||
from collections import defaultdict, deque
|
||||
import logging
|
||||
import time
|
||||
from enum import unique, Flag
|
||||
from typing import DefaultDict, Dict, List
|
||||
from collections import defaultdict, deque
|
||||
from enum import Flag, unique
|
||||
from itertools import chain
|
||||
from math import ceil
|
||||
from typing import DefaultDict, Dict, List
|
||||
|
||||
from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
|
||||
from BaseClasses import PotFlags, LocationType, Direction, KeyRuleType
|
||||
import RaceRandom as random
|
||||
from BaseClasses import (
|
||||
CrystalBarrier,
|
||||
Direction,
|
||||
Door,
|
||||
DoorType,
|
||||
DungeonInfo,
|
||||
KeyRuleType,
|
||||
LocationType,
|
||||
PotFlags,
|
||||
Region,
|
||||
RegionType,
|
||||
Sector,
|
||||
dungeon_keys,
|
||||
)
|
||||
from Doors import reset_portals
|
||||
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
|
||||
from Dungeons import dungeon_bigs, dungeon_hints
|
||||
from DungeonGenerator import (
|
||||
ExplorationState,
|
||||
connect_doors,
|
||||
convert_regions,
|
||||
count_reserved_locations,
|
||||
create_dungeon_builders,
|
||||
default_dungeon_entrances,
|
||||
determine_required_paths,
|
||||
drop_entrances,
|
||||
dungeon_drops,
|
||||
dungeon_portals,
|
||||
simple_dungeon_builder,
|
||||
split_dungeon_builder,
|
||||
valid_region_to_explore,
|
||||
)
|
||||
from Dungeons import (
|
||||
dungeon_bigs,
|
||||
dungeon_hints,
|
||||
dungeon_regions,
|
||||
region_starts,
|
||||
split_region_starts,
|
||||
standard_starts,
|
||||
)
|
||||
from Items import ItemFactory
|
||||
from KeyDoorShuffle import (
|
||||
DoorRules,
|
||||
analyze_dungeon,
|
||||
build_key_layout,
|
||||
determine_prize_lock,
|
||||
validate_bk_layout,
|
||||
validate_key_layout,
|
||||
)
|
||||
from RoomData import DoorKind, PairedDoor, reset_rooms
|
||||
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
|
||||
from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2
|
||||
from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances
|
||||
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
||||
from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations
|
||||
from DungeonGenerator import valid_region_to_explore
|
||||
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
|
||||
from KeyDoorShuffle import validate_bk_layout, DoorRules
|
||||
from Utils import ncr, kth_combination
|
||||
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
|
||||
from Utils import kth_combination, ncr
|
||||
|
||||
|
||||
def link_doors(world, player):
|
||||
@@ -105,6 +141,9 @@ def link_doors_prep(world, player):
|
||||
world.get_portal('Turtle Rock Eye Bridge', player).destination = True
|
||||
else:
|
||||
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]:
|
||||
connect_portal(portal, world, player)
|
||||
|
||||
@@ -234,11 +273,21 @@ def vanilla_key_logic(world, player):
|
||||
enabled_entrances = world.enabled_entrances[player] = {}
|
||||
builder_queue = deque(builders)
|
||||
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:
|
||||
builder = builder_queue.popleft()
|
||||
origin_list = entrances_map[builder.name]
|
||||
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
|
||||
if len(origin_list) <= 0:
|
||||
all_potential_origins = all_potential_portals_map[builder.name]
|
||||
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:
|
||||
origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name
|
||||
if len(origin_list) > 0 else 'no origin')
|
||||
@@ -311,6 +360,33 @@ def validate_vanilla_reservation(dungeon, 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
|
||||
oppositemap = {
|
||||
Direction.South: Direction.North,
|
||||
@@ -1524,7 +1600,11 @@ def assign_cross_keys(dungeon_builders, world, player):
|
||||
if actual_chest_keys == 0:
|
||||
dungeon.small_keys = []
|
||||
else:
|
||||
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
|
||||
extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100)
|
||||
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}')
|
||||
|
||||
|
||||
@@ -2096,7 +2176,10 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_
|
||||
if actual_chest_keys == 0:
|
||||
dungeon.small_keys = []
|
||||
else:
|
||||
dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(actual_chest_keys)]
|
||||
extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100)
|
||||
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():
|
||||
used_doors.update(flatten_pair_list(small_list))
|
||||
@@ -3686,7 +3769,7 @@ logical_connections = [
|
||||
('PoD Pit Room Block Path S', 'PoD Pit Room'),
|
||||
('PoD Arena Landing Bonk Path', 'PoD Arena Bridge'),
|
||||
('PoD Arena North Drop Down', 'PoD Arena Main'),
|
||||
('PoD Arena Bridge Drop Down', 'PoD Arena Main'),
|
||||
('PoD Arena Bridge Drop Down', '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 Landing Barrier - Blue', 'PoD Arena Landing'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
from BaseClasses import Door, DoorType, Direction, CrystalBarrier, Portal
|
||||
from BaseClasses import CrystalBarrier, Direction, Door, DoorType, Portal
|
||||
from RoomData import PairedDoor
|
||||
|
||||
# constants
|
||||
|
||||
+19
-9
@@ -1,22 +1,32 @@
|
||||
import RaceRandom as random
|
||||
import collections
|
||||
import itertools
|
||||
from collections import defaultdict, deque
|
||||
from functools import reduce
|
||||
import logging
|
||||
import math
|
||||
import operator as op
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from functools import reduce
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector
|
||||
from BaseClasses import Hook, hook_from_door, Door
|
||||
from Regions import location_events, flooded_keys_reverse
|
||||
import RaceRandom as random
|
||||
from BaseClasses import (
|
||||
CrystalBarrier,
|
||||
Direction,
|
||||
Door,
|
||||
DoorType,
|
||||
Hook,
|
||||
Polarity,
|
||||
PolSlot,
|
||||
RegionType,
|
||||
Sector,
|
||||
flooded_keys,
|
||||
hook_from_door,
|
||||
)
|
||||
from Dungeons import split_region_starts
|
||||
from Regions import flooded_keys_reverse, location_events
|
||||
from RoomData import DoorKind
|
||||
|
||||
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
|
||||
from source.dungeon.DungeonStitcher import GenerationException as OtherGenException
|
||||
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
|
||||
|
||||
|
||||
class GraphPiece:
|
||||
@@ -2330,7 +2340,7 @@ def parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole):
|
||||
increment_depth = True
|
||||
current_depth = last_depth + 1 if increment_depth else last_depth
|
||||
finished = all([(x.polarity()+sum_polarity(solution_list[x])).is_neutral() for x in builders])
|
||||
logging.getLogger('').info(f'-Balanced solution found in {time.process_time()-start}')
|
||||
logging.getLogger('').debug(f'-Balanced solution found in {time.process_time()-start}')
|
||||
for builder, sectors in solution_list.items():
|
||||
for sector in sectors:
|
||||
assign_sector(sector, builder, polarized_sectors, global_pole)
|
||||
|
||||
@@ -3,19 +3,19 @@ if __name__ == '__main__':
|
||||
from source.meta.check_requirements import check_requirements
|
||||
check_requirements(console=True)
|
||||
|
||||
import os
|
||||
import logging
|
||||
import RaceRandom as random
|
||||
import os
|
||||
import sys
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
import RaceRandom as random
|
||||
import source.classes.diags as diagnostics
|
||||
|
||||
from CLI import parse_cli, get_args_priority
|
||||
from Main import main, EnemizerError, __version__
|
||||
from Rom import get_sprite_from_name
|
||||
from Utils import is_bundled, close_console
|
||||
from CLI import get_args_priority, parse_cli
|
||||
from Fill import FillError
|
||||
from Main import EnemizerError, main
|
||||
from Rom import get_sprite_from_name
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from Utils import close_console, is_bundled
|
||||
|
||||
|
||||
def start():
|
||||
args = parse_cli(None)
|
||||
@@ -80,7 +80,7 @@ def start():
|
||||
break
|
||||
except (FillError, EnemizerError, Exception, RuntimeError) as err:
|
||||
failures.append((err, seed))
|
||||
logger.warning('%s: %s', fish.translate("cli","cli","generation.failed"), err)
|
||||
logger.exception('Attempt %d - %s: %s', trynum, fish.translate("cli","cli","generation.failed"), err)
|
||||
logger.info('')
|
||||
seed = random.randint(0, 999999999)
|
||||
|
||||
|
||||
+14
-2
@@ -13,7 +13,19 @@ def create_dungeons(world, player):
|
||||
dungeon.world = world
|
||||
return dungeon
|
||||
|
||||
ES = make_dungeon('Hyrule Castle', 1, None, hyrule_castle_regions, None, [ItemFactory('Small Key (Escape)', player)], [ItemFactory('Map (Escape)', player)])
|
||||
hc_dungeon_items = ['Map (Escape)']
|
||||
at_dungeon_items = []
|
||||
|
||||
if world.showloot[player] == 'compass':
|
||||
if world.dropshuffle[player] == 'underworld' or world.pottery[player] in ['dungeon', 'reduced', 'clustered', 'nonempty', 'lottery']:
|
||||
hc_dungeon_items.append('Compass (Escape)')
|
||||
at_dungeon_items.append('Compass (Agahnims Tower)')
|
||||
elif world.compassshuffle[player] == 'wild':
|
||||
hc_dungeon_items.append('Compass (Escape)')
|
||||
if world.keyshuffle[player] == 'wild':
|
||||
at_dungeon_items.append('Compass (Agahnims Tower)')
|
||||
|
||||
ES = make_dungeon('Hyrule Castle', 1, None, hyrule_castle_regions, None, [ItemFactory('Small Key (Escape)', player)], ItemFactory(hc_dungeon_items, player))
|
||||
EP = make_dungeon('Eastern Palace', 2, 'Armos Knights', eastern_regions, ItemFactory('Big Key (Eastern Palace)', player), [], ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
|
||||
DP = make_dungeon('Desert Palace', 3, 'Lanmolas', desert_regions, ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 10, 'Moldorm', hera_regions, ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
@@ -24,7 +36,7 @@ def create_dungeons(world, player):
|
||||
IP = make_dungeon('Ice Palace', 9, 'Kholdstare', ice_regions, ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
|
||||
MM = make_dungeon('Misery Mire', 7, 'Vitreous', mire_regions, ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
|
||||
TR = make_dungeon('Turtle Rock', 12, 'Trinexx', tr_regions, ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
|
||||
AT = make_dungeon('Agahnims Tower', 4, 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
AT = make_dungeon('Agahnims Tower', 4, 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), ItemFactory(at_dungeon_items, player))
|
||||
GT = make_dungeon('Ganons Tower', 13, 'Agahnim2', gt_regions, ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
|
||||
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
|
||||
|
||||
+360
-360
@@ -1,361 +1,361 @@
|
||||
Hint description:
|
||||
|
||||
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
|
||||
|
||||
4 hints for inconvenient entrances.
|
||||
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
|
||||
3 hints for inconvenient item locations.
|
||||
5 hints for valuable items.
|
||||
4 junk hints.
|
||||
|
||||
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
|
||||
|
||||
5 hints for inconvenient item locations.
|
||||
8 hints for valuable items.
|
||||
7 junk hints.
|
||||
|
||||
In the simple, restricted shuffles, these are the ratios:
|
||||
|
||||
2 hints for inconvenient entrances.
|
||||
1 hint for an inconvenient dungeon entrance.
|
||||
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
|
||||
3 hints for inconvenient item locations.
|
||||
5 hints for valuable items.
|
||||
5 junk hints.
|
||||
|
||||
These hints will use the following format:
|
||||
|
||||
Entrance hints go "[Entrance on overworld] leads to [interior]".
|
||||
|
||||
Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass).
|
||||
|
||||
The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots.
|
||||
|
||||
Hint Locations:
|
||||
|
||||
Eastern Palace room before Big Chest
|
||||
Desert Palace bonk torch room
|
||||
Tower of Hera entrance room
|
||||
Tower of Hera Big Chest room
|
||||
Castle Tower after dark rooms
|
||||
Palace of Darkness before Bow section
|
||||
Swamp Palace entryway
|
||||
Thieves' Town upstairs
|
||||
Ice Palace entrance
|
||||
Ice Palace after first drop
|
||||
Ice Palace tall ice floor room
|
||||
Misery Mire cutscene room
|
||||
Turtle Rock entrance
|
||||
Spectacle Rock cave
|
||||
Spiky Hint cave
|
||||
PoD Bdlg NPC
|
||||
Near PoD Storyteller (bug near bomb wall)
|
||||
Dark Sanctuary Storyteller (long room with tables)
|
||||
Near Mire Storyteller (feather duster in winding cave)
|
||||
SE DW Storyteller (owl in winding cave)
|
||||
|
||||
Inconvenient entrance list:
|
||||
|
||||
Skull Woods Final
|
||||
Ice Palace
|
||||
Misery Mire
|
||||
Turtle Rock
|
||||
Ganon's Tower
|
||||
Mimic Ledge
|
||||
SW DM Foothills Cave (mirror from upper Bumper ledge)
|
||||
Hammer Pegs (near purple chest)
|
||||
Super Bomb cracked wall
|
||||
|
||||
Inconvenient location list:
|
||||
|
||||
Swamp left (two chests)
|
||||
Mire left (two chests)
|
||||
Hera basement
|
||||
Eastern Palace Big Key chest (protected by anti-fairies)
|
||||
Thieves' Town Big Chest
|
||||
Ice Palace Big Chest
|
||||
Ganon's Tower Big Chest
|
||||
Purple Chest
|
||||
Spike Cave
|
||||
Magic Bat
|
||||
Sahasrahla (Green Pendant)
|
||||
|
||||
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
|
||||
|
||||
Graveyard Cave
|
||||
Mimic Cave
|
||||
|
||||
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
|
||||
|
||||
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
|
||||
|
||||
Overworld Entrance naming:
|
||||
|
||||
Links House: The hero's old residence
|
||||
Turtle Rock: Turtle Rock Main
|
||||
Misery Mire: Misery Mire
|
||||
Ice Palace: Ice Palace
|
||||
Skull Woods Final Section: The back of Skull Woods
|
||||
Death Mountain Return Cave (West): The SW DM Foothills Cave
|
||||
Mimic Cave: Mimic Ledge
|
||||
Hammer Peg Cave: The rows of pegs
|
||||
Pyramid Fairy: The crack on the pyramid
|
||||
Eastern Palace: Eastern Palace
|
||||
Elder House (East): Elder House
|
||||
Elder House (West): Elder House
|
||||
Two Brothers House (East): Eastern Quarreling Brothers' house
|
||||
Old Man Cave (West): The lower DM entrance
|
||||
Hyrule Castle Entrance (South): The ground level castle door
|
||||
Thieves Town: Thieves' Town
|
||||
Bumper Cave (Bottom): The lower Bumper Cave
|
||||
Swamp Palace: Swamp Palace
|
||||
Dark Death Mountain Ledge (West): The East dark DM connector ledge
|
||||
Dark Death Mountain Ledge (East): The East dark DM connector ledge
|
||||
Superbunny Cave (Top): The summit of dark DM cave
|
||||
Superbunny Cave (Bottom): The base of east dark DM
|
||||
Hookshot Cave: The rock on dark DM
|
||||
Desert Palace Entrance (South): The book sealed passage
|
||||
Tower of Hera: The Tower of Hera
|
||||
Two Brothers House (West): The door near the race game
|
||||
Old Man Cave (East): The SW-most cave on west DM
|
||||
Old Man House (Bottom): A cave with a door on west DM
|
||||
Old Man House (Top): The eastmost cave on west DM
|
||||
Death Mountain Return Cave (East): The westmost cave on west DM
|
||||
Spectacle Rock Cave Peak: The highest cave on west DM
|
||||
Spectacle Rock Cave: The right ledge on west DM
|
||||
Spectacle Rock Cave (Bottom): The left ledge on west DM
|
||||
Paradox Cave (Bottom): The right paired cave on east DM
|
||||
Paradox Cave (Middle): The southmost cave on east DM
|
||||
Paradox Cave (Top): The east DM summit cave
|
||||
Fairy Ascension Cave (Bottom): The east DM cave behind rocks
|
||||
Fairy Ascension Cave (Top): The central ledge on east DM
|
||||
Spiral Cave: The left ledge on east DM
|
||||
Spiral Cave (Bottom): The SWmost cave on east DM
|
||||
Palace of Darkness: Palace of Darkness
|
||||
Hyrule Castle Entrance (West): The left castle door
|
||||
Hyrule Castle Entrance (East): The right castle door
|
||||
Agahnims Tower: The sealed castle door
|
||||
Desert Palace Entrance (West): The westmost building in the desert
|
||||
Desert Palace Entrance (North): The northmost cave in the desert
|
||||
Blinds Hideout: Blind's old house
|
||||
Lake Hylia Fairy: A cave NE of Lake Hylia
|
||||
Light Hype Fairy: The cave south of your house
|
||||
Desert Fairy: The cave near the desert
|
||||
Chicken House: The chicken lady's house
|
||||
Tavern North: A backdoor
|
||||
Aginahs Cave: The open desert cave
|
||||
Sahasrahlas Hut: The house near armos
|
||||
Lake Hylia Shop: The cave NW Lake Hylia
|
||||
Blacksmiths Hut: The old smithery
|
||||
Sick Kids House: The central house in Kakariko
|
||||
Lost Woods Gamble: A tree trunk door
|
||||
Fortune Teller (Light): A building NE of Kakariko
|
||||
Snitch Lady (East): A house guarded by a snitch
|
||||
Snitch Lady (West): A house guarded by a snitch
|
||||
Bush Covered House: A house with an uncut lawn
|
||||
Tavern (Front): A building with a backdoor
|
||||
Light World Bomb Hut: A Kakariko building with no door
|
||||
Kakariko Shop: The old Kakariko shop
|
||||
Mini Moldorm Cave: The cave south of Lake Hylia
|
||||
Long Fairy Cave: The eastmost portal cave
|
||||
Good Bee Cave: The open cave SE Lake Hylia
|
||||
20 Rupee Cave: The rock SE Lake Hylia
|
||||
50 Rupee Cave: The rock near the desert
|
||||
Ice Rod Cave: The sealed cave SE Lake Hylia
|
||||
Library: The old library
|
||||
Potion Shop: The witch's building
|
||||
Dam: The old dam
|
||||
Lumberjack House: The lumberjack house
|
||||
Lake Hylia Fortune Teller: The building NW Lake Hylia
|
||||
Kakariko Gamble Game: The old Kakariko gambling den
|
||||
Waterfall of Wishing: Going behind the waterfall
|
||||
Capacity Upgrade: The cave on the island
|
||||
Bonk Rock Cave: The rock pile near Sanctuary
|
||||
Graveyard Cave: The graveyard ledge
|
||||
Checkerboard Cave: The NE desert ledge
|
||||
Cave 45: The ledge south of haunted grove
|
||||
Kings Grave: The northeastmost grave
|
||||
Bonk Fairy (Light): The rock pile near your home
|
||||
Hookshot Fairy: A cave on east DM
|
||||
Bonk Fairy (Dark): The rock pile near the old bomb shop
|
||||
Dark Sanctuary Hint: The dark sanctuary cave
|
||||
Dark Lake Hylia Fairy: The cave NE dark Lake Hylia
|
||||
C-Shaped House: The NE house in Village of Outcasts
|
||||
Big Bomb Shop: The old bomb shop
|
||||
Dark Death Mountain Fairy: The SW cave on dark DM
|
||||
Dark Lake Hylia Shop: The building NW dark Lake Hylia
|
||||
Dark World Shop: The hammer sealed building
|
||||
Red Shield Shop: The fenced in building
|
||||
Mire Shed: The western hut in the mire
|
||||
East Dark World Hint: The dark cave near the eastmost portal
|
||||
Mire Hint: The cave east of the mire
|
||||
Spike Cave: The ledge cave on west dark DM
|
||||
Palace of Darkness Hint: The building south of Kiki
|
||||
Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia
|
||||
Dark Death Mountain Shop: The base of east dark DM
|
||||
Dark Potion Shop: The building near the catfish
|
||||
Archery Game: The old archery game
|
||||
Dark Lumberjack Shop: The northmost Dark World building
|
||||
Hype Cave: The cave south of the old bomb shop
|
||||
Brewery: The Village of Outcasts building with no door
|
||||
Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia
|
||||
Chest Game: The westmost building in the Village of Outcasts
|
||||
Mire Fairy: The eastern hut in the mire
|
||||
Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia
|
||||
Fortune Teller (Dark): The building NE the Village of Outcasts
|
||||
Sanctuary: Sanctuary
|
||||
Lumberjack Tree Cave: The cave Behind Lumberjacks
|
||||
Lost Woods Hideout Stump: The stump in Lost Woods
|
||||
North Fairy Cave: The cave East of Graveyard
|
||||
Bat Cave Cave: The cave in eastern Kakariko
|
||||
Kakariko Well Cave: The cave in northern Kakariko
|
||||
Hyrule Castle Secret Entrance Stairs: The tunnel near the castle
|
||||
Skull Woods First Section Door: The southeastmost skull
|
||||
Skull Woods Second Section Door (East): The central open skull
|
||||
Skull Woods Second Section Door (West): The westmost open skull
|
||||
Desert Palace Entrance (East): The eastern building in the desert
|
||||
Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM
|
||||
Bumper Cave (Top): The upper Bumper Cave
|
||||
Hookshot Cave Back Entrance: The stairs on the floating island
|
||||
|
||||
Destination Entrance Naming:
|
||||
|
||||
Hyrule Castle: Hyrule Castle (all three entrances)
|
||||
Eastern Palace: Eastern Palace
|
||||
Desert Palace: Desert Palace (all four entrances, including final)
|
||||
Tower of Hera: Tower of Hera
|
||||
Palace of Darkness: Palace of Darkness
|
||||
Swamp Palace: Swamp Palace
|
||||
Skull Woods: Skull Woods (any entrance including final)
|
||||
Thieves' Town: Thieves' Town
|
||||
Ice Palace: Ice Palace
|
||||
Misery Mire: Misery Mire
|
||||
Turtle Rock: Turtle Rock (all four entrances)
|
||||
Ganon's Tower: Ganon's Tower
|
||||
Castle Tower: Agahnim's Tower
|
||||
A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave
|
||||
A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout
|
||||
Sahasrahla: Sahasrahla
|
||||
A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy
|
||||
A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave
|
||||
A common shop: Any shop that sells bombs by default
|
||||
The rare shop: The shop that sells the Red Shield by default
|
||||
The potion shop: Potion Shop
|
||||
The bomb shop: Bomb Shop
|
||||
A fortune teller: Any of the three fortune tellers
|
||||
A house with a chest: Chicken Lady's house, C-House, Brewery
|
||||
A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave
|
||||
A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave
|
||||
The dam: Watergate
|
||||
The sick kid: Sick Kid
|
||||
The library: Library
|
||||
Mimic Cave: Mimic Cave
|
||||
Spike Cave: Spike Cave
|
||||
A game of 16 chests: VoO chest game (for the item)
|
||||
A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint
|
||||
A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots)
|
||||
A game of chance: Gambling game (just for cash, no items)
|
||||
A game of skill: Archery minigame
|
||||
The queen of fairies: Capacity Upgrade Fairy
|
||||
A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit
|
||||
A restock room: The Kakariko bomb/arrow restock room
|
||||
The tavern: The Kakariko tavern
|
||||
The grass man: The Kakariko man with many beds
|
||||
A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee
|
||||
Fairies deep in a cave: Hookshot Fairy
|
||||
|
||||
Location naming reference:
|
||||
|
||||
Mushroom: in the woods
|
||||
Master Sword Pedestal: at the pedestal
|
||||
Bottle Merchant: with a merchant
|
||||
Stumpy: with tree boy
|
||||
Flute Spot: underground
|
||||
Digging Game: underground
|
||||
Lake Hylia Island: on an island
|
||||
Floating Island: on an island
|
||||
Bumper Cave Ledge: on a ledge
|
||||
Spectacle Rock: atop a rock
|
||||
Maze Race: at the race
|
||||
Desert Ledge: in the desert
|
||||
Pyramid: on the pyramid
|
||||
Catfish: with a catfish
|
||||
Ether Tablet: at a monument
|
||||
Bombos Tablet: at a monument
|
||||
Hobo: with the hobo
|
||||
Zora's Ledge: near Zora
|
||||
King Zora: at a high price
|
||||
Sunken Treasure: underwater
|
||||
Floodgate Chest: in the dam
|
||||
Blacksmith: with the smith
|
||||
Purple Chest: from a box
|
||||
Old Man: with the old man
|
||||
Link's Uncle: with your uncle
|
||||
Secret Passage: near your uncle
|
||||
Kakariko Well (5 items): in a well
|
||||
Lost Woods Hideout: near a thief
|
||||
Lumberjack Tree: in a hole
|
||||
Magic Bat: with the bat
|
||||
Paradox Cave (7 items): in a cave with seven chests
|
||||
Blind's Hideout (5 items): in a basement
|
||||
Mini Moldorm Cave (5 items): near Moldorms
|
||||
Hype Cave (4 back chests): near a bat-like man
|
||||
Hype Cave - Generous Guy: with a bat-like man
|
||||
Hookshot Cave (4 items): across pits
|
||||
Sahasrahla's Hut (chests in back): near the elder
|
||||
Sahasrahla: with the elder
|
||||
Waterfall Fairy (2 items): near a fairy
|
||||
Pyramid Fairy (2 items): near a fairy
|
||||
Mire Shed (2 items): near sparks
|
||||
Superbunny Cave (2 items): in a connection
|
||||
Spiral Cave: in spiral cave
|
||||
Kakariko Tavern: in the bar
|
||||
Link's House: in your home
|
||||
Sick Kid: with the sick
|
||||
Library: near books
|
||||
Potion Shop: near potions
|
||||
Spike Cave: beyond spikes
|
||||
Mimic Cave: in a cave of mimicry
|
||||
Chest Game: as a game reward
|
||||
Chicken House: near poultry
|
||||
Aginah's Cave: with Aginah
|
||||
Ice Rod Cave: in a frozen cave
|
||||
Brewery: alone in a home
|
||||
C-Shaped House: alone in a home
|
||||
Spectacle Rock Cave: alone in a cave
|
||||
King's Tomb: alone in a cave
|
||||
Cave 45: alone in a cave
|
||||
Graveyard Cave: alone in a cave
|
||||
Checkerboard Cave: alone in a cave
|
||||
Bonk Rock Cave: alone in a cave
|
||||
Peg Cave: alone in a cave
|
||||
Sanctuary: in Sanctuary
|
||||
Hyrule Castle - Boomerang Chest: in Hyrule Castle
|
||||
Hyrule Castle - Map Chest: in Hyrule Castle
|
||||
Hyrule Castle - Zelda's Chest: in Hyrule Castle
|
||||
Sewers - Dark Cross: in the sewers
|
||||
Sewers - Secret Room (3 items): in the sewers
|
||||
Eastern Palace - Boss: with the Armos
|
||||
Eastern Palace (otherwise, 5 items): in Eastern Palace
|
||||
Desert Palace - Boss: with Lanmolas
|
||||
Desert Palace (otherwise, 5 items): in Desert Palace
|
||||
Tower of Hera - Boss: with Moldorm
|
||||
Tower of Hera (otherwise, 5 items): in Tower of Hera
|
||||
Castle Tower (2 items): in Castle Tower
|
||||
Palace of Darkness - Boss: with Helmasaur King
|
||||
Palace of Darkness (otherwise, 13 items): in Palace of Darkness
|
||||
Swamp Palace - Boss: with Arrghus
|
||||
Swamp Palace (otherwise, 9 items): in Swamp Palace
|
||||
Skull Woods - Bridge Room: near Mothula
|
||||
Skull Woods - Boss: with Mothula
|
||||
Skull Woods (otherwise, 6 items): in Skull Woods
|
||||
Thieves' Town - Boss: with Blind
|
||||
Thieves' Town (otherwise, 7 items): in Thieves' Town
|
||||
Ice Palace - Boss: with Kholdstare
|
||||
Ice Palace (otherwise, 7 items): in Ice Palace
|
||||
Misery Mire - Boss: with Vitreous
|
||||
Misery Mire (otherwise, 7 items): in Misery Mire
|
||||
Turtle Rock - Boss: with Trinexx
|
||||
Turtle Rock (otherwise, 11 items): in Turtle Rock
|
||||
Ganons Tower (after climb, 4 items): atop Ganon's Tower
|
||||
Hint description:
|
||||
|
||||
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
|
||||
|
||||
4 hints for inconvenient entrances.
|
||||
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
|
||||
3 hints for inconvenient item locations.
|
||||
5 hints for valuable items.
|
||||
4 junk hints.
|
||||
|
||||
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
|
||||
|
||||
5 hints for inconvenient item locations.
|
||||
8 hints for valuable items.
|
||||
7 junk hints.
|
||||
|
||||
In the simple, restricted shuffles, these are the ratios:
|
||||
|
||||
2 hints for inconvenient entrances.
|
||||
1 hint for an inconvenient dungeon entrance.
|
||||
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
|
||||
3 hints for inconvenient item locations.
|
||||
5 hints for valuable items.
|
||||
5 junk hints.
|
||||
|
||||
These hints will use the following format:
|
||||
|
||||
Entrance hints go "[Entrance on overworld] leads to [interior]".
|
||||
|
||||
Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass).
|
||||
|
||||
The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots.
|
||||
|
||||
Hint Locations:
|
||||
|
||||
Eastern Palace room before Big Chest
|
||||
Desert Palace bonk torch room
|
||||
Tower of Hera entrance room
|
||||
Tower of Hera Big Chest room
|
||||
Castle Tower after dark rooms
|
||||
Palace of Darkness before Bow section
|
||||
Swamp Palace entryway
|
||||
Thieves' Town upstairs
|
||||
Ice Palace entrance
|
||||
Ice Palace after first drop
|
||||
Ice Palace tall ice floor room
|
||||
Misery Mire cutscene room
|
||||
Turtle Rock entrance
|
||||
Spectacle Rock cave
|
||||
Spiky Hint cave
|
||||
PoD Bdlg NPC
|
||||
Near PoD Storyteller (bug near bomb wall)
|
||||
Dark Sanctuary Storyteller (long room with tables)
|
||||
Near Mire Storyteller (feather duster in winding cave)
|
||||
SE DW Storyteller (owl in winding cave)
|
||||
|
||||
Inconvenient entrance list:
|
||||
|
||||
Skull Woods Final
|
||||
Ice Palace
|
||||
Misery Mire
|
||||
Turtle Rock
|
||||
Ganon's Tower
|
||||
Mimic Ledge
|
||||
SW DM Foothills Cave (mirror from upper Bumper ledge)
|
||||
Hammer Pegs (near purple chest)
|
||||
Super Bomb cracked wall
|
||||
|
||||
Inconvenient location list:
|
||||
|
||||
Swamp left (two chests)
|
||||
Mire left (two chests)
|
||||
Hera basement
|
||||
Eastern Palace Big Key chest (protected by anti-fairies)
|
||||
Thieves' Town Big Chest
|
||||
Ice Palace Big Chest
|
||||
Ganon's Tower Big Chest
|
||||
Purple Chest
|
||||
Spike Cave
|
||||
Magic Bat
|
||||
Sahasrahla (Green Pendant)
|
||||
|
||||
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
|
||||
|
||||
Graveyard Cave
|
||||
Mimic Cave
|
||||
|
||||
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
|
||||
|
||||
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
|
||||
|
||||
Overworld Entrance naming:
|
||||
|
||||
Links House: The hero's old residence
|
||||
Turtle Rock: Turtle Rock Main
|
||||
Misery Mire: Misery Mire
|
||||
Ice Palace: Ice Palace
|
||||
Skull Woods Final Section: The back of Skull Woods
|
||||
Death Mountain Return Cave (West): The SW DM Foothills Cave
|
||||
Mimic Cave: Mimic Ledge
|
||||
Hammer Peg Cave: The rows of pegs
|
||||
Pyramid Fairy: The crack on the pyramid
|
||||
Eastern Palace: Eastern Palace
|
||||
Elder House (East): Elder House
|
||||
Elder House (West): Elder House
|
||||
Two Brothers House (East): Eastern Quarreling Brothers' house
|
||||
Old Man Cave (West): The lower DM entrance
|
||||
Hyrule Castle Entrance (South): The ground level castle door
|
||||
Thieves Town: Thieves' Town
|
||||
Bumper Cave (Bottom): The lower Bumper Cave
|
||||
Swamp Palace: Swamp Palace
|
||||
Dark Death Mountain Ledge (West): The East dark DM connector ledge
|
||||
Dark Death Mountain Ledge (East): The East dark DM connector ledge
|
||||
Superbunny Cave (Top): The summit of dark DM cave
|
||||
Superbunny Cave (Bottom): The base of east dark DM
|
||||
Hookshot Cave: The rock on dark DM
|
||||
Desert Palace Entrance (South): The book sealed passage
|
||||
Tower of Hera: The Tower of Hera
|
||||
Two Brothers House (West): The door near the race game
|
||||
Old Man Cave (East): The SW-most cave on west DM
|
||||
Old Man House (Bottom): A cave with a door on west DM
|
||||
Old Man House (Top): The eastmost cave on west DM
|
||||
Death Mountain Return Cave (East): The westmost cave on west DM
|
||||
Spectacle Rock Cave Peak: The highest cave on west DM
|
||||
Spectacle Rock Cave: The right ledge on west DM
|
||||
Spectacle Rock Cave (Bottom): The left ledge on west DM
|
||||
Paradox Cave (Bottom): The right paired cave on east DM
|
||||
Paradox Cave (Middle): The southmost cave on east DM
|
||||
Paradox Cave (Top): The east DM summit cave
|
||||
Fairy Ascension Cave (Bottom): The east DM cave behind rocks
|
||||
Fairy Ascension Cave (Top): The central ledge on east DM
|
||||
Spiral Cave: The left ledge on east DM
|
||||
Spiral Cave (Bottom): The SWmost cave on east DM
|
||||
Palace of Darkness: Palace of Darkness
|
||||
Hyrule Castle Entrance (West): The left castle door
|
||||
Hyrule Castle Entrance (East): The right castle door
|
||||
Agahnims Tower: The sealed castle door
|
||||
Desert Palace Entrance (West): The westmost building in the desert
|
||||
Desert Palace Entrance (North): The northmost cave in the desert
|
||||
Blinds Hideout: Blind's old house
|
||||
Lake Hylia Fairy: A cave NE of Lake Hylia
|
||||
Light Hype Fairy: The cave south of your house
|
||||
Desert Fairy: The cave near the desert
|
||||
Chicken House: The chicken lady's house
|
||||
Tavern North: A backdoor
|
||||
Aginahs Cave: The open desert cave
|
||||
Sahasrahlas Hut: The house near armos
|
||||
Lake Hylia Shop: The cave NW Lake Hylia
|
||||
Blacksmiths Hut: The old smithery
|
||||
Sick Kids House: The central house in Kakariko
|
||||
Lost Woods Gamble: A tree trunk door
|
||||
Fortune Teller (Light): A building NE of Kakariko
|
||||
Snitch Lady (East): A house guarded by a snitch
|
||||
Snitch Lady (West): A house guarded by a snitch
|
||||
Bush Covered House: A house with an uncut lawn
|
||||
Tavern (Front): A building with a backdoor
|
||||
Light World Bomb Hut: A Kakariko building with no door
|
||||
Kakariko Shop: The old Kakariko shop
|
||||
Mini Moldorm Cave: The cave south of Lake Hylia
|
||||
Long Fairy Cave: The eastmost portal cave
|
||||
Good Bee Cave: The open cave SE Lake Hylia
|
||||
20 Rupee Cave: The rock SE Lake Hylia
|
||||
50 Rupee Cave: The rock near the desert
|
||||
Ice Rod Cave: The sealed cave SE Lake Hylia
|
||||
Library: The old library
|
||||
Potion Shop: The witch's building
|
||||
Dam: The old dam
|
||||
Lumberjack House: The lumberjack house
|
||||
Lake Hylia Fortune Teller: The building NW Lake Hylia
|
||||
Kakariko Gamble Game: The old Kakariko gambling den
|
||||
Waterfall of Wishing: Going behind the waterfall
|
||||
Capacity Upgrade: The cave on the island
|
||||
Bonk Rock Cave: The rock pile near Sanctuary
|
||||
Graveyard Cave: The graveyard ledge
|
||||
Checkerboard Cave: The NE desert ledge
|
||||
Cave 45: The ledge south of haunted grove
|
||||
Kings Grave: The northeastmost grave
|
||||
Bonk Fairy (Light): The rock pile near your home
|
||||
Hookshot Fairy: A cave on east DM
|
||||
Bonk Fairy (Dark): The rock pile near the old bomb shop
|
||||
Dark Sanctuary Hint: The dark sanctuary cave
|
||||
Dark Lake Hylia Fairy: The cave NE dark Lake Hylia
|
||||
C-Shaped House: The NE house in Village of Outcasts
|
||||
Big Bomb Shop: The old bomb shop
|
||||
Dark Death Mountain Fairy: The SW cave on dark DM
|
||||
Dark Lake Hylia Shop: The building NW dark Lake Hylia
|
||||
Dark World Shop: The hammer sealed building
|
||||
Red Shield Shop: The fenced in building
|
||||
Mire Shed: The western hut in the mire
|
||||
East Dark World Hint: The dark cave near the eastmost portal
|
||||
Mire Hint: The cave east of the mire
|
||||
Spike Cave: The ledge cave on west dark DM
|
||||
Palace of Darkness Hint: The building south of Kiki
|
||||
Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia
|
||||
Dark Death Mountain Shop: The base of east dark DM
|
||||
Dark Potion Shop: The building near the catfish
|
||||
Archery Game: The old archery game
|
||||
Dark Lumberjack Shop: The northmost Dark World building
|
||||
Hype Cave: The cave south of the old bomb shop
|
||||
Brewery: The Village of Outcasts building with no door
|
||||
Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia
|
||||
Chest Game: The westmost building in the Village of Outcasts
|
||||
Mire Fairy: The eastern hut in the mire
|
||||
Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia
|
||||
Fortune Teller (Dark): The building NE the Village of Outcasts
|
||||
Sanctuary: Sanctuary
|
||||
Lumberjack Tree Cave: The cave Behind Lumberjacks
|
||||
Lost Woods Hideout Stump: The stump in Lost Woods
|
||||
North Fairy Cave: The cave East of Graveyard
|
||||
Bat Cave Cave: The cave in eastern Kakariko
|
||||
Kakariko Well Cave: The cave in northern Kakariko
|
||||
Hyrule Castle Secret Entrance Stairs: The tunnel near the castle
|
||||
Skull Woods First Section Door: The southeastmost skull
|
||||
Skull Woods Second Section Door (East): The central open skull
|
||||
Skull Woods Second Section Door (West): The westmost open skull
|
||||
Desert Palace Entrance (East): The eastern building in the desert
|
||||
Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM
|
||||
Bumper Cave (Top): The upper Bumper Cave
|
||||
Hookshot Cave Back Entrance: The stairs on the floating island
|
||||
|
||||
Destination Entrance Naming:
|
||||
|
||||
Hyrule Castle: Hyrule Castle (all three entrances)
|
||||
Eastern Palace: Eastern Palace
|
||||
Desert Palace: Desert Palace (all four entrances, including final)
|
||||
Tower of Hera: Tower of Hera
|
||||
Palace of Darkness: Palace of Darkness
|
||||
Swamp Palace: Swamp Palace
|
||||
Skull Woods: Skull Woods (any entrance including final)
|
||||
Thieves' Town: Thieves' Town
|
||||
Ice Palace: Ice Palace
|
||||
Misery Mire: Misery Mire
|
||||
Turtle Rock: Turtle Rock (all four entrances)
|
||||
Ganon's Tower: Ganon's Tower
|
||||
Castle Tower: Agahnim's Tower
|
||||
A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave
|
||||
A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout
|
||||
Sahasrahla: Sahasrahla
|
||||
A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy
|
||||
A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave
|
||||
A common shop: Any shop that sells bombs by default
|
||||
The rare shop: The shop that sells the Red Shield by default
|
||||
The potion shop: Potion Shop
|
||||
The bomb shop: Bomb Shop
|
||||
A fortune teller: Any of the three fortune tellers
|
||||
A house with a chest: Chicken Lady's house, C-House, Brewery
|
||||
A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave
|
||||
A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave
|
||||
The dam: Watergate
|
||||
The sick kid: Sick Kid
|
||||
The library: Library
|
||||
Mimic Cave: Mimic Cave
|
||||
Spike Cave: Spike Cave
|
||||
A game of 16 chests: VoO chest game (for the item)
|
||||
A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint
|
||||
A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots)
|
||||
A game of chance: Gambling game (just for cash, no items)
|
||||
A game of skill: Archery minigame
|
||||
The queen of fairies: Capacity Upgrade Fairy
|
||||
A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit
|
||||
A restock room: The Kakariko bomb/arrow restock room
|
||||
The tavern: The Kakariko tavern
|
||||
The grass man: The Kakariko man with many beds
|
||||
A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee
|
||||
Fairies deep in a cave: Hookshot Fairy
|
||||
|
||||
Location naming reference:
|
||||
|
||||
Mushroom: in the woods
|
||||
Master Sword Pedestal: at the pedestal
|
||||
Bottle Merchant: with a merchant
|
||||
Stumpy: with tree boy
|
||||
Flute Spot: underground
|
||||
Digging Game: underground
|
||||
Lake Hylia Island: on an island
|
||||
Floating Island: on an island
|
||||
Bumper Cave Ledge: on a ledge
|
||||
Spectacle Rock: atop a rock
|
||||
Maze Race: at the race
|
||||
Desert Ledge: in the desert
|
||||
Pyramid: on the pyramid
|
||||
Catfish: with a catfish
|
||||
Ether Tablet: at a monument
|
||||
Bombos Tablet: at a monument
|
||||
Hobo: with the hobo
|
||||
Zora's Ledge: near Zora
|
||||
King Zora: at a high price
|
||||
Sunken Treasure: underwater
|
||||
Floodgate Chest: in the dam
|
||||
Blacksmith: with the smith
|
||||
Purple Chest: from a box
|
||||
Old Man: with the old man
|
||||
Link's Uncle: with your uncle
|
||||
Secret Passage: near your uncle
|
||||
Kakariko Well (5 items): in a well
|
||||
Lost Woods Hideout: near a thief
|
||||
Lumberjack Tree: in a hole
|
||||
Magic Bat: with the bat
|
||||
Paradox Cave (7 items): in a cave with seven chests
|
||||
Blind's Hideout (5 items): in a basement
|
||||
Mini Moldorm Cave (5 items): near Moldorms
|
||||
Hype Cave (4 back chests): near a bat-like man
|
||||
Hype Cave - Generous Guy: with a bat-like man
|
||||
Hookshot Cave (4 items): across pits
|
||||
Sahasrahla's Hut (chests in back): near the elder
|
||||
Sahasrahla: with the elder
|
||||
Waterfall Fairy (2 items): near a fairy
|
||||
Pyramid Fairy (2 items): near a fairy
|
||||
Mire Shed (2 items): near sparks
|
||||
Superbunny Cave (2 items): in a connection
|
||||
Spiral Cave: in spiral cave
|
||||
Kakariko Tavern: in the bar
|
||||
Link's House: in your home
|
||||
Sick Kid: with the sick
|
||||
Library: near books
|
||||
Potion Shop: near potions
|
||||
Spike Cave: beyond spikes
|
||||
Mimic Cave: in a cave of mimicry
|
||||
Chest Game: as a game reward
|
||||
Chicken House: near poultry
|
||||
Aginah's Cave: with Aginah
|
||||
Ice Rod Cave: in a frozen cave
|
||||
Brewery: alone in a home
|
||||
C-Shaped House: alone in a home
|
||||
Spectacle Rock Cave: alone in a cave
|
||||
King's Tomb: alone in a cave
|
||||
Cave 45: alone in a cave
|
||||
Graveyard Cave: alone in a cave
|
||||
Checkerboard Cave: alone in a cave
|
||||
Bonk Rock Cave: alone in a cave
|
||||
Peg Cave: alone in a cave
|
||||
Sanctuary: in Sanctuary
|
||||
Hyrule Castle - Boomerang Chest: in Hyrule Castle
|
||||
Hyrule Castle - Map Chest: in Hyrule Castle
|
||||
Hyrule Castle - Zelda's Chest: in Hyrule Castle
|
||||
Sewers - Dark Cross: in the sewers
|
||||
Sewers - Secret Room (3 items): in the sewers
|
||||
Eastern Palace - Boss: with the Armos
|
||||
Eastern Palace (otherwise, 5 items): in Eastern Palace
|
||||
Desert Palace - Boss: with Lanmolas
|
||||
Desert Palace (otherwise, 5 items): in Desert Palace
|
||||
Tower of Hera - Boss: with Moldorm
|
||||
Tower of Hera (otherwise, 5 items): in Tower of Hera
|
||||
Castle Tower (2 items): in Castle Tower
|
||||
Palace of Darkness - Boss: with Helmasaur King
|
||||
Palace of Darkness (otherwise, 13 items): in Palace of Darkness
|
||||
Swamp Palace - Boss: with Arrghus
|
||||
Swamp Palace (otherwise, 9 items): in Swamp Palace
|
||||
Skull Woods - Bridge Room: near Mothula
|
||||
Skull Woods - Boss: with Mothula
|
||||
Skull Woods (otherwise, 6 items): in Skull Woods
|
||||
Thieves' Town - Boss: with Blind
|
||||
Thieves' Town (otherwise, 7 items): in Thieves' Town
|
||||
Ice Palace - Boss: with Kholdstare
|
||||
Ice Palace (otherwise, 7 items): in Ice Palace
|
||||
Misery Mire - Boss: with Vitreous
|
||||
Misery Mire (otherwise, 7 items): in Misery Mire
|
||||
Turtle Rock - Boss: with Trinexx
|
||||
Turtle Rock (otherwise, 11 items): in Turtle Rock
|
||||
Ganons Tower (after climb, 4 items): atop Ganon's Tower
|
||||
Ganon's Tower (otherwise, 23 items): in Ganon's Tower
|
||||
@@ -1,4 +1,3 @@
|
||||
import RaceRandom as random
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
@@ -6,11 +5,18 @@ import math
|
||||
from collections import Counter
|
||||
from contextlib import suppress
|
||||
|
||||
import RaceRandom as random
|
||||
from BaseClasses import CollectionState, FillError, LocationType
|
||||
from Items import ItemFactory
|
||||
from Regions import shop_to_location_table, retro_shops
|
||||
from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback
|
||||
from source.item.FillUtil import filter_special_locations, valid_pot_items
|
||||
from Regions import retro_shops, shop_to_location_table
|
||||
from source.item.FillUtil import (
|
||||
classify_major_items,
|
||||
filter_locations,
|
||||
filter_special_locations,
|
||||
replace_trash_item,
|
||||
valid_pot_items,
|
||||
vanilla_fallback,
|
||||
)
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
@@ -111,7 +117,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
for attempt in range(15):
|
||||
try:
|
||||
for player in range(1, world.players + 1):
|
||||
if world.prizeshuffle[player] == 'nearby':
|
||||
if world.prizeshuffle[player] == 'nearby' and world.algorithm != 'vanilla_fill':
|
||||
dungeon_pool = []
|
||||
for dungeon in world.dungeons:
|
||||
from Dungeons import dungeon_table
|
||||
@@ -143,6 +149,14 @@ def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
else:
|
||||
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)
|
||||
fill(all_state_base, others, shuffled_locations)
|
||||
|
||||
@@ -234,9 +248,11 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
|
||||
test_state.sweep_for_events()
|
||||
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 (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):
|
||||
return location
|
||||
|
||||
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize:
|
||||
location.item = None
|
||||
location.event = False
|
||||
@@ -296,7 +312,9 @@ def valid_dungeon_placement(item, location, world):
|
||||
dungeon = check_dungeon
|
||||
if dungeon:
|
||||
layout = world.dungeon_layouts[location.player][dungeon.name]
|
||||
if not is_dungeon_item(item, world) or item.player != location.player:
|
||||
if item.event:
|
||||
return True
|
||||
elif not is_dungeon_item(item, world) or item.player != location.player:
|
||||
if item.prize and item.is_near_dungeon_item(world):
|
||||
return item.dungeon_object == dungeon and layout.free_items > 0
|
||||
return layout.free_items > 0
|
||||
@@ -330,6 +348,10 @@ def track_dungeon_items(item, location, world):
|
||||
if item.prize:
|
||||
location.parent_region.dungeon.prize = item
|
||||
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):
|
||||
@@ -722,8 +744,13 @@ def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
|
||||
flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count
|
||||
fill_count = len(pot_fill_locations[player]) - flex
|
||||
if fill_count > 0:
|
||||
fill_spots = random.sample(pot_fill_locations[player], fill_count)
|
||||
fill_items = random.sample(pot_item_pool[player], fill_count)
|
||||
first_count = min(fill_count, len(pot_item_pool[player]))
|
||||
fill_items = random.sample(pot_item_pool[player], first_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:
|
||||
item_pool.remove(x)
|
||||
for x in fill_spots:
|
||||
@@ -793,7 +820,7 @@ def sell_potions(world, player):
|
||||
if shop.region.name in shop_to_location_table and shop.region.name != 'Capacity Upgrade':
|
||||
loc_choices += [world.get_location(loc, player) for loc in shop_to_location_table[shop.region.name]]
|
||||
locations = [loc for loc in loc_choices if not loc.item]
|
||||
for potion in ['Green Potion', 'Blue Potion', 'Red Potion']:
|
||||
for potion in ['Green Potion', 'Blue Potion', 'Red Potion', 'Bee']:
|
||||
location = random.choice(filter_locations(ItemFactory(potion, player), locations, world, potion=True))
|
||||
locations.remove(location)
|
||||
p_item = next((item for item in world.itempool if item.name == potion and item.player == player), None)
|
||||
@@ -1024,7 +1051,10 @@ def balance_money_progression(world):
|
||||
'Rupees (100)': 100, 'Rupees (300)': 300}
|
||||
rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90,
|
||||
'TR Rupees': 270, 'PoD Dark Basement': 270}
|
||||
acceptable_balancers = ['Bombs (3)', 'Arrows (10)', 'Bombs (10)']
|
||||
acceptable_balancers = ['Single Bomb', 'Bombs (3)', 'Bombs (10)',
|
||||
'Single Arrow', 'Arrows (5)', 'Arrows (10)',
|
||||
'Small Magic', 'Big Magic', 'Small Heart',
|
||||
'Fairy', 'Chicken', 'Nothing']
|
||||
|
||||
base_value = sum(rupee_rooms.values())
|
||||
available_money = {player: base_value for player in range(1, world.players+1)}
|
||||
@@ -1054,6 +1084,8 @@ 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)]
|
||||
|
||||
def interesting_item(location, item, world, player):
|
||||
if location.event or location.locked:
|
||||
return True
|
||||
if item.advancement:
|
||||
return True
|
||||
if item.type is not None or item.name.startswith('Rupee'):
|
||||
@@ -1101,7 +1133,7 @@ def balance_money_progression(world):
|
||||
slot = shop_to_location_table[location.parent_region.name].index(location.name)
|
||||
shop = location.parent_region.shop
|
||||
shop_item = shop.inventory[slot]
|
||||
if location.item and interesting_item(location, location.item, world, location.item.player):
|
||||
if shop_item and 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 shop_item['price'] < rupee_chart[location.item.name]:
|
||||
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block
|
||||
@@ -1123,10 +1155,11 @@ def balance_money_progression(world):
|
||||
unchecked_locations.remove(location)
|
||||
if location.item:
|
||||
if location.item.name.startswith('Rupee'):
|
||||
wallet[location.item.player] += rupee_chart[location.item.name]
|
||||
if location.item.name != 'Rupees (300)':
|
||||
balance_locations[location.item.player].add(location)
|
||||
if interesting_item(location, location.item, world, location.item.player):
|
||||
if not (location.item.name == 'Rupee (1)' and world.algorithm != 'district'):
|
||||
wallet[location.item.player] += rupee_chart[location.item.name]
|
||||
if location.item.name != 'Rupees (300)':
|
||||
balance_locations[location.item.player].add(location)
|
||||
elif interesting_item(location, location.item, world, location.item.player):
|
||||
checked_locations.append(location)
|
||||
elif location.item.name in acceptable_balancers:
|
||||
balance_locations[location.item.player].add(location)
|
||||
@@ -1170,7 +1203,11 @@ def balance_money_progression(world):
|
||||
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]
|
||||
if len(increase_targets) == 0:
|
||||
raise Exception('No early sphere swaps for rupees - money grind would be required - bailing for now')
|
||||
if state.can_farm_rupees(target_player):
|
||||
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)
|
||||
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)
|
||||
@@ -1287,4 +1324,4 @@ def set_prize_drops(world, player):
|
||||
# saved fish prize
|
||||
world.prizes[player]['fish'] = prizes.pop()
|
||||
|
||||
world.prizes[player]['enemies'] = prizes
|
||||
world.prizes[player]['enemies'] = prizes
|
||||
|
||||
@@ -5,36 +5,47 @@ if __name__ == '__main__':
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from tkinter import Tk, Button, BOTTOM, TOP, StringVar, BooleanVar, X, BOTH, RIGHT, ttk, messagebox
|
||||
from tkinter import (
|
||||
BOTH,
|
||||
BOTTOM,
|
||||
RIGHT,
|
||||
TOP,
|
||||
BooleanVar,
|
||||
Button,
|
||||
StringVar,
|
||||
Tk,
|
||||
X,
|
||||
messagebox,
|
||||
ttk,
|
||||
)
|
||||
|
||||
from CLI import get_args_priority
|
||||
from DungeonRandomizer import parse_cli
|
||||
from GuiUtils import set_icon
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from source.classes.Empty import Empty
|
||||
from source.gui.adjust.overview import adjust_page
|
||||
from source.gui.startinventory.overview import startinventory_page
|
||||
from source.gui.bottom import bottom_frame, create_guiargs
|
||||
from source.gui.custom.overview import custom_page
|
||||
from source.gui.loadcliargs import loadcliargs, loadadjustargs
|
||||
from source.gui.randomize.item import item_page
|
||||
from source.gui.randomize.overworld import overworld_page
|
||||
from source.gui.randomize.entrando import entrando_page
|
||||
from source.gui.randomize.enemizer import enemizer_page
|
||||
from source.gui.loadcliargs import loadadjustargs, loadcliargs
|
||||
from source.gui.randomize.dungeon import dungeon_page
|
||||
from source.gui.randomize.enemizer import enemizer_page
|
||||
from source.gui.randomize.entrando import entrando_page
|
||||
|
||||
#from source.gui.randomize.multiworld import multiworld_page
|
||||
from source.gui.randomize.gameoptions import gameoptions_page
|
||||
from source.gui.randomize.generation import generation_page
|
||||
from source.gui.bottom import bottom_frame, create_guiargs
|
||||
from GuiUtils import set_icon
|
||||
from Main import __version__ as ESVersion
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from source.classes.Empty import Empty
|
||||
from source.gui.randomize.item import item_page
|
||||
from source.gui.randomize.overworld import overworld_page
|
||||
from source.gui.startinventory.overview import startinventory_page
|
||||
from Versions import DRVersion, GKVersion, ORVersion
|
||||
|
||||
|
||||
def check_python_version(fish):
|
||||
import sys
|
||||
version = sys.version_info
|
||||
if version.major < 3 or version.minor < 7:
|
||||
messagebox.showinfo("Overworld Shuffle %s (DR %s)" % (ORVersion, ESVersion), fish.translate("cli","cli","old.python.version") % sys.version)
|
||||
messagebox.showinfo("GwaaKiwi Randomizer %s (OR %s, DR %s)" % (GKVersion, ORVersion, DRVersion), fish.translate("cli","cli","old.python.version") % sys.version)
|
||||
|
||||
|
||||
# Save settings to file
|
||||
@@ -83,7 +94,7 @@ def guiMain(args=None):
|
||||
mainWindow = Tk()
|
||||
self = mainWindow
|
||||
|
||||
mainWindow.wm_title("Overworld Shuffle %s (DR %s)" % (ORVersion, ESVersion))
|
||||
mainWindow.wm_title("GwaaKiwi Randomizer %s (OR %s, DR %s)" % (GKVersion, ORVersion, DRVersion))
|
||||
mainWindow.protocol("WM_DELETE_WINDOW", guiExit) # intercept when user clicks the X
|
||||
|
||||
# set program icon
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
import queue
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import tkinter as tk
|
||||
|
||||
from Utils import local_path
|
||||
|
||||
|
||||
def set_icon(window):
|
||||
er16 = tk.PhotoImage(file=local_path(os.path.join("data","ER16.gif")))
|
||||
er32 = tk.PhotoImage(file=local_path(os.path.join("data","ER32.gif")))
|
||||
|
||||
+136
-61
@@ -1,21 +1,39 @@
|
||||
from collections import namedtuple, defaultdict
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
import RaceRandom as random
|
||||
|
||||
from BaseClasses import LocationType, Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem
|
||||
from Regions import location_events, shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location
|
||||
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys
|
||||
from PotShuffle import vanilla_pots
|
||||
from Tables import bonk_prize_lookup
|
||||
from Items import ItemFactory
|
||||
|
||||
from source.dungeon.EnemyList import add_drop_contents
|
||||
from source.overworld.EntranceShuffle2 import exit_ids, door_addresses
|
||||
from source.item.FillUtil import trash_items, pot_items
|
||||
|
||||
import source.classes.constants as CONST
|
||||
|
||||
from BaseClasses import (
|
||||
CollectionState,
|
||||
Location,
|
||||
LocationType,
|
||||
PotItem,
|
||||
Region,
|
||||
RegionType,
|
||||
Shop,
|
||||
ShopType,
|
||||
)
|
||||
from Fill import (
|
||||
FillError,
|
||||
fill_restrictive,
|
||||
get_dungeon_item_pool,
|
||||
track_dungeon_items,
|
||||
track_outside_keys,
|
||||
)
|
||||
from Items import ItemFactory
|
||||
from PotShuffle import vanilla_pots
|
||||
from Regions import (
|
||||
location_events,
|
||||
retro_shops,
|
||||
shop_table_by_location,
|
||||
shop_to_location_table,
|
||||
valid_pot_location,
|
||||
)
|
||||
from source.dungeon.EnemyList import add_drop_contents
|
||||
from source.item.FillUtil import pot_items, trash_items
|
||||
from source.overworld.EntranceShuffle2 import door_addresses, exit_ids
|
||||
from Tables import bonk_prize_lookup
|
||||
|
||||
#This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||
#Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||
@@ -25,8 +43,8 @@ alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod
|
||||
progressivegloves = ['Progressive Glove'] * 2
|
||||
basicgloves = ['Power Glove', 'Titans Mitts']
|
||||
|
||||
normalbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)']
|
||||
hardbottles = ['Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Bee)', 'Bottle (Good Bee)']
|
||||
normalbottles = ['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)']
|
||||
|
||||
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)
|
||||
@@ -218,12 +236,14 @@ def get_custom_array_key(item):
|
||||
|
||||
def generate_itempool(world, player):
|
||||
if (world.difficulty[player] not in ['normal', 'hard', 'expert']
|
||||
or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals',
|
||||
'ganonhunt', 'completionist', 'sanctuary']
|
||||
or world.goal[player] not in ['ganon', 'pedestal', 'dungeons',
|
||||
'triforcehunt', 'trinity', 'crystals',
|
||||
'ganonhunt', 'completionist', 'sanctuary',
|
||||
'bosshunt']
|
||||
or world.mode[player] not in ['open', 'standard', 'inverted']
|
||||
or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']
|
||||
or world.progressive not in ['on', 'off', 'random']):
|
||||
raise NotImplementedError('Not supported yet')
|
||||
raise NotImplementedError('Not supported yet')
|
||||
|
||||
if world.timer in ['ohko', 'timed-ohko']:
|
||||
world.can_take_damage[player] = False
|
||||
@@ -305,6 +325,42 @@ def generate_itempool(world, player):
|
||||
for _ in range(0, amt):
|
||||
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'] \
|
||||
and world.keyshuffle[player] not in ['none', 'nearby']:
|
||||
# In HMG force swamp smalls in pots to allow getting out of swamp palace
|
||||
@@ -316,6 +372,13 @@ def generate_itempool(world, player):
|
||||
world.push_precollected(ItemFactory(item, 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:
|
||||
found_sword = False
|
||||
found_bow = False
|
||||
@@ -344,24 +407,14 @@ def generate_itempool(world, player):
|
||||
world.escape_assist[player].append('bombs')
|
||||
|
||||
for (location, item) in placed_items.items():
|
||||
world.push_item(world.get_location(location, player), ItemFactory(item, player), False)
|
||||
world.get_location(location, player).event = True
|
||||
world.get_location(location, player).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)
|
||||
loc = world.get_location(location, player)
|
||||
world.push_item(loc, ItemFactory(item, player), False)
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
items = ItemFactory(pool, player)
|
||||
if world.shopsanity[player]:
|
||||
for potion in ['Green Potion', 'Blue Potion', 'Red Potion']:
|
||||
for potion in ['Green Potion', 'Blue Potion', 'Red Potion', 'Bee']:
|
||||
p_item = next(item for item in items if item.name == potion and item.player == player)
|
||||
p_item.priority = True # don't beemize one of each potion
|
||||
|
||||
@@ -443,11 +496,11 @@ def generate_itempool(world, player):
|
||||
|
||||
# shuffle bottle refills
|
||||
if world.difficulty[player] in ['hard', 'expert']:
|
||||
waterfall_bottle = hardbottles[random.randint(0, 5)]
|
||||
pyramid_bottle = hardbottles[random.randint(0, 5)]
|
||||
waterfall_bottle = hardbottles[random.randint(0, 4)]
|
||||
pyramid_bottle = hardbottles[random.randint(0, 4)]
|
||||
else:
|
||||
waterfall_bottle = normalbottles[random.randint(0, 6)]
|
||||
pyramid_bottle = normalbottles[random.randint(0, 6)]
|
||||
waterfall_bottle = normalbottles[random.randint(0, 5)]
|
||||
pyramid_bottle = normalbottles[random.randint(0, 5)]
|
||||
world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle)
|
||||
|
||||
set_up_shops(world, player)
|
||||
@@ -691,7 +744,7 @@ def create_farm_locations(world, player):
|
||||
world.dynamic_locations.append(loc)
|
||||
return loc
|
||||
|
||||
from Rules import set_rule, add_rule, add_bunny_rule
|
||||
from Rules import add_bunny_rule, add_rule, set_rule
|
||||
for region in bush_bombs:
|
||||
loc = create_and_fill_location(region, 'Bush Drop', 'Farmable Bombs')
|
||||
add_bunny_rule(loc, player)
|
||||
@@ -1107,7 +1160,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
|
||||
precollected_items.append('Pegasus Boots')
|
||||
pool.remove('Pegasus Boots')
|
||||
pool.extend(['Rupees (20)'])
|
||||
|
||||
|
||||
if want_progressives():
|
||||
pool.extend(progressivegloves)
|
||||
else:
|
||||
@@ -1487,7 +1540,7 @@ def make_customizer_pool(world, player):
|
||||
elif timer == 'ohko':
|
||||
clock_mode = 'ohko'
|
||||
|
||||
if goal in ['sanctuary']:
|
||||
if world.goal[player] in ['sanctuary']:
|
||||
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:
|
||||
place_item('Master Sword Pedestal', 'Nothing')
|
||||
@@ -1502,7 +1555,7 @@ def make_customizer_pool(world, player):
|
||||
guaranteed_items.append('Ocarina (Activated)')
|
||||
missing_items = []
|
||||
if world.shopsanity[player]:
|
||||
guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion'])
|
||||
guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion', 'Bee'])
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
guaranteed_items.append('Small Key (Universal)')
|
||||
for item in guaranteed_items:
|
||||
@@ -1623,12 +1676,15 @@ def fill_specific_items(world):
|
||||
item_to_place, event_flag = get_item_and_event_flag(item, world, player,
|
||||
dungeon_pool, prize_set, prize_pool)
|
||||
if item_to_place:
|
||||
world.push_item(loc, item_to_place, False)
|
||||
loc.locked = True
|
||||
track_outside_keys(item_to_place, loc, world)
|
||||
track_dungeon_items(item_to_place, loc, world)
|
||||
loc.event = (event_flag or item_to_place.advancement
|
||||
or item_to_place.bigkey or item_to_place.smallkey)
|
||||
if not loc.item:
|
||||
world.push_item(loc, item_to_place, False)
|
||||
loc.locked = True
|
||||
track_outside_keys(item_to_place, loc, world)
|
||||
track_dungeon_items(item_to_place, loc, world)
|
||||
loc.event = (event_flag or item_to_place.advancement
|
||||
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:
|
||||
raise Exception(f'Did not find "{item}" in item pool to place at "{location}"')
|
||||
advanced_placements = world.customizer.get_advanced_placements()
|
||||
@@ -1707,6 +1763,7 @@ def shuffle_event_items(world, player):
|
||||
available_quests = follower_quests.copy()
|
||||
available_pickups = [quests[0] for quests in available_quests.values()]
|
||||
|
||||
# finalize customizer followers first
|
||||
for loc_name in follower_quests.keys():
|
||||
loc = world.get_location(loc_name, player)
|
||||
if loc.item:
|
||||
@@ -1714,7 +1771,6 @@ def shuffle_event_items(world, player):
|
||||
available_quests.pop(loc_name)
|
||||
available_pickups.remove(loc.item.name)
|
||||
|
||||
|
||||
if world.mode[player] == 'standard' and 'Zelda Herself' in available_pickups:
|
||||
zelda_dropoff = 'Zelda Pickup'
|
||||
if world.default_zelda_region[player] == 'Thieves Blind\'s Cell':
|
||||
@@ -1724,20 +1780,39 @@ def shuffle_event_items(world, player):
|
||||
available_pickups.remove(zelda_pickup)
|
||||
set_event_item(world, player, zelda_dropoff, zelda_pickup)
|
||||
|
||||
random.shuffle(available_pickups)
|
||||
follower_locations = [world.get_location(loc_name, player) for loc_name in available_quests.keys()]
|
||||
|
||||
restricted_pickups = { 'Get Frog': 'Dark Blacksmith Ruins'}
|
||||
for pickup in restricted_pickups:
|
||||
restricted_quests = [q for q in available_quests.keys() if q not in restricted_pickups[pickup]]
|
||||
random.shuffle(restricted_quests)
|
||||
quest = restricted_quests.pop()
|
||||
available_quests.pop(quest)
|
||||
available_pickups.remove(pickup)
|
||||
set_event_item(world, player, quest, pickup)
|
||||
attempts = 10
|
||||
for attempt in range(attempts):
|
||||
try:
|
||||
all_state = world.get_all_state(keys=True)
|
||||
if world.prizeshuffle[player] != 'wild':
|
||||
from Items import prize_item_table
|
||||
prizes = ItemFactory(list(prize_item_table.keys()), player)
|
||||
for prize in prizes:
|
||||
all_state.collect(prize, True)
|
||||
|
||||
for pickup in available_pickups:
|
||||
quest, _ = available_quests.popitem()
|
||||
set_event_item(world, player, quest, pickup)
|
||||
# randomize the follower pickups, but ensure that the last items are the unrestrictive ones
|
||||
unrestrictive_pickups = ItemFactory([p for p in ['Zelda Herself', 'Sign Vandalized'] if p in available_pickups], player)
|
||||
restrictive_pickups = ItemFactory([p for p in available_pickups if p not in unrestrictive_pickups], player)
|
||||
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):
|
||||
|
||||
@@ -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'),
|
||||
'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'),
|
||||
'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'),
|
||||
'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'),
|
||||
'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 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'),
|
||||
|
||||
+10
-4
@@ -2,11 +2,17 @@ import itertools
|
||||
import logging
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
|
||||
from BaseClasses import DoorType, KeyRuleType, RegionType, dungeon_keys
|
||||
from DungeonGenerator import (
|
||||
ExplorationState,
|
||||
blind_boss_unavail,
|
||||
count_locations_exclude_big_chest,
|
||||
get_special_big_key_doors,
|
||||
prize_or_event,
|
||||
reserved_location,
|
||||
)
|
||||
from Dungeons import dungeon_bigs, dungeon_keys, dungeon_table
|
||||
from Regions import location_events
|
||||
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table
|
||||
from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
|
||||
from DungeonGenerator import reserved_location, blind_boss_unavail
|
||||
|
||||
|
||||
class KeyLayout(object):
|
||||
|
||||
@@ -1,50 +1,100 @@
|
||||
import base64
|
||||
import copy
|
||||
from itertools import zip_longest
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import RaceRandom as random
|
||||
import string
|
||||
import time
|
||||
import zlib
|
||||
import base64
|
||||
from itertools import zip_longest
|
||||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance, Settings
|
||||
import RaceRandom as random
|
||||
from BaseClasses import (
|
||||
CollectionState,
|
||||
Entrance,
|
||||
Item,
|
||||
Location,
|
||||
Region,
|
||||
Settings,
|
||||
Shop,
|
||||
World,
|
||||
)
|
||||
from Bosses import place_bosses
|
||||
from Doors import create_doors
|
||||
from DoorShuffle import connect_portal, link_doors, link_doors_prep
|
||||
from Dungeons import create_dungeons
|
||||
from Fill import (
|
||||
balance_money_progression,
|
||||
balance_multiworld_progression,
|
||||
distribute_items_restrictive,
|
||||
dungeon_tracking,
|
||||
ensure_good_items,
|
||||
fill_dungeons_restrictive,
|
||||
lock_shop_locations,
|
||||
promote_dungeon_items,
|
||||
sell_keys,
|
||||
sell_potions,
|
||||
set_prize_drops,
|
||||
)
|
||||
from ItemList import (
|
||||
create_farm_locations,
|
||||
customize_shops,
|
||||
difficulties,
|
||||
fill_prizes,
|
||||
fill_specific_items,
|
||||
follower_pickups,
|
||||
generate_itempool,
|
||||
shuffle_event_items,
|
||||
)
|
||||
from Items import ItemFactory
|
||||
from KeyDoorShuffle import validate_key_placement
|
||||
from OverworldGlitchRules import create_owg_connections
|
||||
from PotShuffle import shuffle_pots, shuffle_pot_switches
|
||||
from Regions import create_regions, create_shops, mark_light_dark_world_regions, create_dungeon_regions, adjust_locations
|
||||
from OverworldShuffle import (
|
||||
create_dynamic_mirror_exits,
|
||||
link_overworld,
|
||||
update_world_regions,
|
||||
)
|
||||
from OWEdges import create_owedges
|
||||
from OverworldShuffle import link_overworld, update_world_regions, create_dynamic_flute_exits, create_dynamic_mirror_exits
|
||||
from Rom import patch_rom, patch_race_rom, apply_rom_settings, LocalRom, JsonRom, get_hash_string
|
||||
from Doors import create_doors
|
||||
from DoorShuffle import link_doors, connect_portal, link_doors_prep
|
||||
from PotShuffle import shuffle_pot_switches, shuffle_pots
|
||||
from Regions import (
|
||||
adjust_locations,
|
||||
create_dungeon_regions,
|
||||
create_regions,
|
||||
create_shops,
|
||||
mark_light_dark_world_regions,
|
||||
)
|
||||
from Rom import (
|
||||
JsonRom,
|
||||
LocalRom,
|
||||
apply_rom_settings,
|
||||
apply_rom_patches,
|
||||
get_hash_string,
|
||||
patch_race_rom,
|
||||
patch_rom,
|
||||
)
|
||||
from RoomData import create_rooms
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons
|
||||
from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_items
|
||||
from Fill import dungeon_tracking
|
||||
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations, shuffle_event_items, follower_pickups
|
||||
from UnderworldGlitchRules import connect_hmg_entrances_regions, create_hmg_entrances_regions
|
||||
from Utils import output_path, parse_player_names
|
||||
|
||||
from source.item.District import init_districts
|
||||
from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config, verify_item_pool_config
|
||||
from source.overworld.EntranceShuffle2 import link_entrances_new
|
||||
from source.tools.BPS import create_bps_from_data
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
from source.enemizer.DamageTables import DamageTable
|
||||
from source.enemizer.Enemizer import randomize_enemies
|
||||
from source.item.District import init_districts
|
||||
from source.item.FillUtil import (
|
||||
create_item_pool_config,
|
||||
district_item_pool_config,
|
||||
massage_item_pool,
|
||||
verify_item_pool_config,
|
||||
)
|
||||
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
|
||||
|
||||
version_number = '1.5.0'
|
||||
version_branch = '-u'
|
||||
__version__ = f'{version_number}{version_branch}'
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from source.tools.BPS import create_bps_from_data
|
||||
from UnderworldGlitchRules import (
|
||||
connect_hmg_entrances_regions,
|
||||
create_hmg_entrances_regions,
|
||||
)
|
||||
from Utils import output_path, parse_player_names
|
||||
from Versions import DRVersion, GKVersion, ORVersion
|
||||
|
||||
|
||||
class EnemizerError(RuntimeError):
|
||||
@@ -80,7 +130,7 @@ def random_ganon_item(sword_mode):
|
||||
|
||||
def main(args, seed=None, fish=None):
|
||||
check_python_version()
|
||||
|
||||
|
||||
if args.print_template_yaml:
|
||||
return export_yaml(args, fish)
|
||||
|
||||
@@ -122,16 +172,14 @@ def main(args, seed=None, fish=None):
|
||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||
world.finish_init()
|
||||
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
logger.info(
|
||||
world.fish.translate("cli","cli","app.title") + "\n",
|
||||
ORVersion,
|
||||
"%s (%s)" % (world.seed, str(args.outputname)) if str(args.outputname).startswith('M') else world.seed,
|
||||
Settings.make_code(world, 1) if world.players == 1 else ''
|
||||
world.fish.translate("cli","cli","app.title") + "\n",
|
||||
GKVersion,
|
||||
"%s (%s)" % (world.seed, str(args.outputname)) if str(args.outputname).startswith('M') else world.seed,
|
||||
)
|
||||
|
||||
for k,v in {"DR":__version__,"OR":ORVersion}.items():
|
||||
logger.info((k + ' Version:').ljust(16) + '%s' % v)
|
||||
for k,v in {"GK": GKVersion, "OR": ORVersion, "DR": DRVersion}.items():
|
||||
logger.info((k + ' Version:').ljust(16) + '%s' % v)
|
||||
|
||||
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||
world.teams = len(parsed_names)
|
||||
@@ -142,7 +190,7 @@ def main(args, seed=None, fish=None):
|
||||
world.player_names[player].append(name)
|
||||
logger.info('')
|
||||
|
||||
outfilebase = f'OR_{args.outputname if args.outputname else world.seed}'
|
||||
outfilebase = f'GK_{args.outputname if args.outputname else world.seed}'
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
@@ -288,7 +336,7 @@ def main(args, seed=None, fish=None):
|
||||
for player in range(1, world.players+1):
|
||||
if world.shopsanity[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)
|
||||
ensure_good_items(world, True)
|
||||
|
||||
@@ -317,6 +365,8 @@ def main(args, seed=None, fish=None):
|
||||
rom_names.append((player, team, list(rom.name)))
|
||||
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],
|
||||
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],
|
||||
@@ -421,15 +471,13 @@ def export_yaml(args, fish):
|
||||
if args.seed and int(args.seed) > 0:
|
||||
world.seed = int(args.seed)
|
||||
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
logger.info(
|
||||
world.fish.translate("cli","cli","app.title") + "\n",
|
||||
ORVersion,
|
||||
GKVersion,
|
||||
"(%s)" % outfilebase,
|
||||
Settings.make_code(world, 1) if world.players == 1 else ''
|
||||
)
|
||||
|
||||
for k,v in {"DR":__version__,"OR":ORVersion}.items():
|
||||
for k,v in {"GK": GKVersion, "OR": ORVersion, "DR": DRVersion}.items():
|
||||
logger.info((k + ' Version:').ljust(16) + '%s' % v)
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
@@ -458,7 +506,7 @@ def init_world(args, fish):
|
||||
customized.load_yaml(args.customizer)
|
||||
customized.adjust_args(args, False)
|
||||
|
||||
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
|
||||
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,
|
||||
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)
|
||||
|
||||
@@ -470,16 +518,22 @@ def init_world(args, fish):
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.prizeshuffle = args.prizeshuffle.copy()
|
||||
world.showloot = args.showloot.copy()
|
||||
world.loothud = args.loothud.copy()
|
||||
world.showmap = args.showmap.copy()
|
||||
world.bombbag = args.bombbag.copy()
|
||||
world.flute_mode = args.flute_mode.copy()
|
||||
world.bow_mode = args.bow_mode.copy()
|
||||
world.crystals_ganon_orig = args.crystals_ganon.copy()
|
||||
world.crystals_gt_orig = args.crystals_gt.copy()
|
||||
world.ganon_item_orig = args.ganon_item.copy()
|
||||
world.bosses_ganon = {player: int(args.bosses_ganon[player]) for player in range(1, world.players + 1)}
|
||||
world.bosshunt_include_agas = args.bosshunt_include_agas.copy()
|
||||
world.owTerrain = args.ow_terrain.copy()
|
||||
world.owKeepSimilar = args.ow_keepsimilar.copy()
|
||||
world.owWhirlpoolShuffle = args.ow_whirlpool.copy()
|
||||
world.owFluteShuffle = args.ow_fluteshuffle.copy()
|
||||
world.owFog = args.ow_fog.copy()
|
||||
world.shuffle_followers = args.shuffle_followers.copy()
|
||||
world.shuffle_bonk_drops = args.bonk_drops.copy()
|
||||
world.open_pyramid = args.openpyramid.copy()
|
||||
@@ -514,16 +568,17 @@ def init_world(args, fish):
|
||||
world.damage_challenge = args.damage_challenge.copy()
|
||||
world.shuffle_damage_table = args.shuffle_damage_table.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.take_any = args.take_any.copy()
|
||||
world.restrict_boss_items = args.restrict_boss_items.copy()
|
||||
world.collection_rate = args.collection_rate.copy()
|
||||
world.colorizepots = args.colorizepots.copy()
|
||||
world.aga_randomness = args.aga_randomness.copy()
|
||||
world.money_balance = args.money_balance.copy()
|
||||
world.money_balance = {player: int(args.money_balance[player]) for player in range(1, world.players + 1)}
|
||||
|
||||
# custom settings - these haven't been promoted to full settings yet
|
||||
in_progress_settings = ['force_enemy', 'free_lamp_cone']
|
||||
in_progress_settings = ['force_enemy']
|
||||
for player in range(1, world.players + 1):
|
||||
for setting in in_progress_settings:
|
||||
if world.customizer and world.customizer.has_setting(player, setting):
|
||||
@@ -757,7 +812,7 @@ def set_starting_inventory(world, args):
|
||||
|
||||
def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
ret = World(world.players, world.owLayout, world.owParallel, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
|
||||
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
|
||||
ret.teams = world.teams
|
||||
@@ -785,12 +840,17 @@ def copy_world(world):
|
||||
ret.keyshuffle = world.keyshuffle.copy()
|
||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
||||
ret.prizeshuffle = world.prizeshuffle.copy()
|
||||
ret.showloot = world.showloot.copy()
|
||||
ret.loothud = world.loothud.copy()
|
||||
ret.showmap = world.showmap.copy()
|
||||
ret.bombbag = world.bombbag.copy()
|
||||
ret.flute_mode = world.flute_mode.copy()
|
||||
ret.bow_mode = world.bow_mode.copy()
|
||||
ret.free_lamp_cone = world.free_lamp_cone.copy()
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
||||
ret.bosses_ganon = world.bosses_ganon.copy()
|
||||
ret.bosshunt_include_agas = world.bosshunt_include_agas.copy()
|
||||
ret.ganon_item = world.ganon_item.copy()
|
||||
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
|
||||
ret.crystals_gt_orig = world.crystals_gt_orig.copy()
|
||||
@@ -799,6 +859,7 @@ def copy_world(world):
|
||||
ret.owKeepSimilar = world.owKeepSimilar.copy()
|
||||
ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy()
|
||||
ret.owFluteShuffle = world.owFluteShuffle.copy()
|
||||
ret.owFog = world.owFog.copy()
|
||||
ret.shuffle_followers = world.shuffle_followers.copy()
|
||||
ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy()
|
||||
ret.open_pyramid = world.open_pyramid.copy()
|
||||
@@ -813,6 +874,7 @@ def copy_world(world):
|
||||
ret.damage_challenge = world.damage_challenge.copy()
|
||||
ret.shuffle_damage_table = world.shuffle_damage_table.copy()
|
||||
ret.crystal_book = world.crystal_book.copy()
|
||||
ret.extra_keys = world.extra_keys.copy()
|
||||
ret.overworld_map = world.overworld_map.copy()
|
||||
ret.take_any = world.take_any.copy()
|
||||
ret.boss_shuffle = world.boss_shuffle.copy()
|
||||
@@ -984,7 +1046,7 @@ def copy_world(world):
|
||||
|
||||
def copy_world_premature(world, player, create_flute_exits=True):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
ret = World(world.players, world.owLayout, world.owParallel, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
|
||||
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode)
|
||||
ret.teams = world.teams
|
||||
@@ -1012,12 +1074,17 @@ def copy_world_premature(world, player, create_flute_exits=True):
|
||||
ret.keyshuffle = world.keyshuffle.copy()
|
||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
||||
ret.prizeshuffle = world.prizeshuffle.copy()
|
||||
ret.showloot = world.showloot.copy()
|
||||
ret.loothud = world.loothud.copy()
|
||||
ret.showmap = world.showmap.copy()
|
||||
ret.bombbag = world.bombbag.copy()
|
||||
ret.flute_mode = world.flute_mode.copy()
|
||||
ret.bow_mode = world.bow_mode.copy()
|
||||
ret.free_lamp_cone = world.free_lamp_cone.copy()
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
||||
ret.bosses_ganon = world.bosses_ganon.copy()
|
||||
ret.bosshunt_include_agas = world.bosshunt_include_agas.copy()
|
||||
ret.ganon_item = world.ganon_item.copy()
|
||||
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
|
||||
ret.crystals_gt_orig = world.crystals_gt_orig.copy()
|
||||
@@ -1026,6 +1093,7 @@ def copy_world_premature(world, player, create_flute_exits=True):
|
||||
ret.owKeepSimilar = world.owKeepSimilar.copy()
|
||||
ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy()
|
||||
ret.owFluteShuffle = world.owFluteShuffle.copy()
|
||||
ret.owFog = world.owFog.copy()
|
||||
ret.shuffle_followers = world.shuffle_followers.copy()
|
||||
ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy()
|
||||
ret.open_pyramid = world.open_pyramid.copy()
|
||||
@@ -1040,6 +1108,7 @@ def copy_world_premature(world, player, create_flute_exits=True):
|
||||
ret.damage_challenge = world.damage_challenge.copy()
|
||||
ret.shuffle_damage_table = world.shuffle_damage_table.copy()
|
||||
ret.crystal_book = world.crystal_book.copy()
|
||||
ret.extra_keys = world.extra_keys.copy()
|
||||
ret.overworld_map = world.overworld_map.copy()
|
||||
ret.take_any = world.take_any.copy()
|
||||
ret.boss_shuffle = world.boss_shuffle.copy()
|
||||
@@ -1222,6 +1291,9 @@ def create_playthrough(world):
|
||||
for location in sphere:
|
||||
if world.goal[location.player] == 'completionist':
|
||||
continue # every location for that player is required
|
||||
if location.item.type == "SmallKey":
|
||||
continue
|
||||
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
||||
old_item = location.item
|
||||
|
||||
+6
-5
@@ -1,19 +1,20 @@
|
||||
import aioconsole
|
||||
import argparse
|
||||
import asyncio
|
||||
import colorama
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
import urllib.parse
|
||||
|
||||
import aioconsole
|
||||
import colorama
|
||||
import websockets
|
||||
|
||||
from BaseClasses import PotItem, PotFlags, LocationType
|
||||
import Items
|
||||
import Regions
|
||||
import PotShuffle
|
||||
import Regions
|
||||
import source.dungeon.EnemyList as EnemyList
|
||||
import source.rom.DataTables as DataTables
|
||||
from BaseClasses import LocationType, PotFlags, PotItem
|
||||
|
||||
|
||||
class ReceivedItem:
|
||||
@@ -975,8 +976,8 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
||||
ow_unchecked[location] = (screenid, 0x40)
|
||||
ow_begin = min(ow_begin, screenid)
|
||||
ow_end = max(ow_end, screenid + 1)
|
||||
from Regions import bonk_prize_table
|
||||
from OWEdges import OWTileRegions
|
||||
from Regions import bonk_prize_table
|
||||
for location, (_, flag, _, _, region_name, _) in bonk_prize_table.items():
|
||||
if location not in ctx.locations_checked:
|
||||
if region_name in OWTileRegions:
|
||||
|
||||
+11
-5
@@ -1,4 +1,3 @@
|
||||
import aioconsole
|
||||
import argparse
|
||||
import asyncio
|
||||
import functools
|
||||
@@ -8,16 +7,23 @@ import re
|
||||
import shlex
|
||||
import ssl
|
||||
import urllib.request
|
||||
import websockets
|
||||
import zlib
|
||||
|
||||
from BaseClasses import PotItem, PotFlags
|
||||
import aioconsole
|
||||
import websockets
|
||||
|
||||
import Items
|
||||
import Regions
|
||||
import PotShuffle
|
||||
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
|
||||
import Regions
|
||||
import source.dungeon.EnemyList as EnemyList
|
||||
import source.rom.DataTables as DataTables
|
||||
from BaseClasses import PotFlags, PotItem
|
||||
from MultiClient import (
|
||||
ReceivedItem,
|
||||
get_item_name_from_id,
|
||||
get_location_name_from_address,
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, socket):
|
||||
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
import argparse
|
||||
import logging
|
||||
import RaceRandom as random
|
||||
|
||||
from yaml.constructor import SafeConstructor
|
||||
|
||||
import RaceRandom as random
|
||||
from DungeonRandomizer import parse_cli
|
||||
from Main import main as DRMain
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from yaml.constructor import SafeConstructor
|
||||
|
||||
from source.tools.MysteryUtils import roll_settings, get_weights
|
||||
from source.tools.MysteryUtils import get_weights, roll_settings
|
||||
|
||||
|
||||
def add_bool(self, node):
|
||||
|
||||
+11
-1
@@ -1,8 +1,10 @@
|
||||
|
||||
from BaseClasses import OWEdge, Direction, Terrain, WorldType, PolSlot
|
||||
from enum import Enum, unique
|
||||
|
||||
from BaseClasses import Direction, OWEdge, PolSlot, Terrain, WorldType
|
||||
from Utils import bidict
|
||||
|
||||
|
||||
@unique
|
||||
class OpenStd(Enum):
|
||||
Open = 0
|
||||
@@ -324,12 +326,20 @@ def create_owedges(world, player):
|
||||
|
||||
world.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):
|
||||
if name not in OWExitTypes['OWEdge']:
|
||||
OWExitTypes['OWEdge'].append(name)
|
||||
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 = {
|
||||
#(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup)
|
||||
|
||||
+213
-303
@@ -1,19 +1,37 @@
|
||||
import RaceRandom as random, logging, copy
|
||||
import copy
|
||||
import logging
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import RaceRandom as random
|
||||
from BaseClasses import (
|
||||
Direction,
|
||||
Entrance,
|
||||
OWEdge,
|
||||
PolSlot,
|
||||
RegionType,
|
||||
Terrain,
|
||||
WorldType,
|
||||
)
|
||||
from DungeonGenerator import GenerationException
|
||||
from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSlot, Entrance
|
||||
from OverworldGlitchRules import create_owg_connections
|
||||
from OWEdges import (
|
||||
IsParallel,
|
||||
OpenStd,
|
||||
OWEdgeGroups,
|
||||
OWEdgeGroupsTerrain,
|
||||
OWExitTypes,
|
||||
OWTileRegions,
|
||||
parallel_links,
|
||||
)
|
||||
from Regions import mark_light_dark_world_regions
|
||||
from source.overworld.EntranceShuffle2 import connect_simple
|
||||
from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitTypes, OpenStd, parallel_links, IsParallel
|
||||
from OverworldGlitchRules import create_owg_connections
|
||||
from source.overworld.FluteShuffle import (
|
||||
default_flute_connections,
|
||||
flute_data,
|
||||
shuffle_flute_spots,
|
||||
)
|
||||
from Utils import bidict
|
||||
|
||||
version_number = '0.6.1.7'
|
||||
# branch indicator is intentionally different across branches
|
||||
version_branch = ''
|
||||
|
||||
__version__ = '%s%s' % (version_number, version_branch)
|
||||
|
||||
parallel_links_new = None # needs to be globally available, reset every new generation/player
|
||||
|
||||
def link_overworld(world, player):
|
||||
@@ -114,37 +132,38 @@ def link_overworld(world, player):
|
||||
|
||||
# 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)
|
||||
if world.owKeepSimilar[player]:
|
||||
del parallel_links_new['Maze Race ES']
|
||||
del parallel_links_new['Kakariko Suburb WS']
|
||||
for group in trimmed_groups.keys():
|
||||
(std, region, axis, terrain, parallel, _, custom) = group
|
||||
if parallel == IsParallel.Yes:
|
||||
if world.owLayout[player] != 'grid':
|
||||
if world.owKeepSimilar[player]:
|
||||
del parallel_links_new['Maze Race ES']
|
||||
del parallel_links_new['Kakariko Suburb WS']
|
||||
for group in trimmed_groups.keys():
|
||||
(std, region, axis, terrain, parallel, _, custom) = group
|
||||
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]
|
||||
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'])
|
||||
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)
|
||||
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()})}
|
||||
|
||||
connected_edges = []
|
||||
if world.owShuffle[player] != 'vanilla':
|
||||
if world.owLayout[player] != 'vanilla':
|
||||
trimmed_groups = remove_reserved(world, trimmed_groups, connected_edges, player)
|
||||
trimmed_groups = reorganize_groups(world, trimmed_groups, player)
|
||||
|
||||
@@ -232,7 +251,7 @@ def link_overworld(world, player):
|
||||
if 'undefined_chance' in custom_crossed:
|
||||
undefined_chance = custom_crossed['undefined_chance']
|
||||
|
||||
if limited_crossed > -1:
|
||||
if limited_crossed > -1 and world.owLayout[player] != 'grid':
|
||||
# connect forced crossed non-parallel edges based on previously determined tile flips
|
||||
for edge in swapped_edges:
|
||||
if edge not in parallel_links_new:
|
||||
@@ -264,10 +283,10 @@ def link_overworld(world, player):
|
||||
s[0x30], s[0x35],
|
||||
s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f])
|
||||
world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player)
|
||||
elif limited_crossed > -1 or (world.owShuffle[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
|
||||
elif (limited_crossed > -1 and world.owLayout[player] != 'grid') or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
|
||||
crossed_candidates = list()
|
||||
for group in trimmed_groups.keys():
|
||||
(mode, wrld, dir, terrain, parallel, count, _) = group
|
||||
(mode, wrld, _, terrain, parallel, _, _) = group
|
||||
if wrld == WorldType.Light and mode != OpenStd.Standard:
|
||||
for (forward_set, back_set) in zip(trimmed_groups[group][0], trimmed_groups[group][1]):
|
||||
if forward_set[0] in parallel_links_new:
|
||||
@@ -278,7 +297,7 @@ def link_overworld(world, player):
|
||||
combine_set = forward_combine+back_combine
|
||||
|
||||
skip_forward = False
|
||||
if world.owShuffle[player] == 'vanilla':
|
||||
if world.owLayout[player] == 'vanilla':
|
||||
if any(edge in force_crossed 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):
|
||||
@@ -335,6 +354,7 @@ def link_overworld(world, player):
|
||||
world.owcrossededges[player].extend(edge_set)
|
||||
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]):
|
||||
if edge in parallel_links_new:
|
||||
if parallel_links_new[edge] not in world.owcrossededges[player]:
|
||||
@@ -412,7 +432,7 @@ def link_overworld(world, player):
|
||||
# layout shuffle
|
||||
logging.getLogger('').debug('Shuffling overworld layout')
|
||||
|
||||
if world.owShuffle[player] == 'vanilla':
|
||||
if world.owLayout[player] == 'vanilla':
|
||||
# apply outstanding flips
|
||||
trimmed_groups = performSwap(trimmed_groups, edges_to_swap)
|
||||
assert len(edges_to_swap) == 0, 'Not all edges were flipped successfully: ' + ', '.join(edges_to_swap)
|
||||
@@ -425,8 +445,15 @@ def link_overworld(world, player):
|
||||
assert len(forward_set) == len(back_set)
|
||||
for (forward_edge, back_edge) in zip(forward_set, back_set):
|
||||
connect_two_way(world, forward_edge, back_edge, player, connected_edges)
|
||||
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:
|
||||
if world.owKeepSimilar[player] and world.owShuffle[player] == 'parallel':
|
||||
if world.owKeepSimilar[player] and world.owParallel[player]:
|
||||
for exitname, destname in parallelsimilar_connections:
|
||||
connect_two_way(world, exitname, destname, player, connected_edges)
|
||||
|
||||
@@ -557,7 +584,7 @@ def link_overworld(world, player):
|
||||
remove_connected(forward_edge_sets, back_edge_sets)
|
||||
assert len(connected_edges) == len(default_connections) * 2, connected_edges
|
||||
|
||||
valid_layout = validate_layout(world, player)
|
||||
valid_layout = world.accessibility[player] == 'none' or validate_layout(world, player)
|
||||
|
||||
tries -= 1
|
||||
assert valid_layout, 'Could not find a valid OW layout'
|
||||
@@ -566,166 +593,8 @@ def link_overworld(world, player):
|
||||
|
||||
# flute shuffle
|
||||
logging.getLogger('').debug('Shuffling flute spots')
|
||||
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 = []
|
||||
shuffle_flute_spots(world, player)
|
||||
|
||||
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):
|
||||
forced_crossed, forced_noncrossed = forced
|
||||
@@ -822,7 +691,7 @@ def connect_custom(world, connected_edges, groups, forced, player):
|
||||
remove_pair_from_pool(edge1.name, edge2.name, is_crossed)
|
||||
connect_two_way(world, edge1.name, edge2.name, player, connected_edges)
|
||||
# resolve parallel
|
||||
if world.owShuffle[player] == 'parallel' and edge1.name in parallel_links_new:
|
||||
if world.owParallel[player] and edge1.name in parallel_links_new:
|
||||
parallel_forward_edge = parallel_links_new[edge1.name]
|
||||
parallel_back_edge = parallel_links_new[edge2.name]
|
||||
if validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
|
||||
@@ -838,13 +707,13 @@ def connect_custom(world, connected_edges, groups, forced, player):
|
||||
connect_two_way(world, forward_edge, back_edge, player, connected_edges)
|
||||
else:
|
||||
raise GenerationException('Violation of force crossed rules on unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
|
||||
if world.owShuffle[player] == 'parallel' and forward_edge in parallel_links_new:
|
||||
if world.owParallel[player] and forward_edge in parallel_links_new:
|
||||
parallel_forward_edge = parallel_links_new[forward_edge]
|
||||
parallel_back_edge = parallel_links_new[back_edge]
|
||||
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)
|
||||
|
||||
def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
|
||||
def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, set_spoiler=True):
|
||||
edge1 = world.get_entrance(edgename1, player)
|
||||
edge2 = world.get_entrance(edgename2, player)
|
||||
x = world.get_owedge(edgename1, player)
|
||||
@@ -868,7 +737,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
|
||||
x.dest = y
|
||||
y.dest = x
|
||||
|
||||
if world.owShuffle[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none':
|
||||
if set_spoiler and (world.owLayout[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none'):
|
||||
world.spoiler.set_overworld(edgename2, edgename1, 'both', player)
|
||||
|
||||
if connected_edges is not None:
|
||||
@@ -876,7 +745,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
|
||||
connected_edges.append(edgename2)
|
||||
|
||||
# connecting parallel connections
|
||||
if world.owShuffle[player] in ['vanilla', 'parallel']:
|
||||
if world.owLayout[player] == 'vanilla' or world.owParallel[player]:
|
||||
if edgename1 in parallel_links_new:
|
||||
try:
|
||||
parallel_forward_edge = parallel_links_new[edgename1]
|
||||
@@ -965,7 +834,7 @@ def determine_forced_flips(world, tile_ow_groups, do_grouped, player):
|
||||
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]):
|
||||
merged_owids.append([whirlpool_map[whirl1], whirlpool_map[whirl2]])
|
||||
if world.owShuffle[player] != 'vanilla':
|
||||
if world.owLayout[player] != 'vanilla':
|
||||
custom_edges = world.customizer.get_owedges()
|
||||
if custom_edges and player in custom_edges:
|
||||
custom_edges = custom_edges[player]
|
||||
@@ -1071,6 +940,9 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player):
|
||||
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)]
|
||||
if world.owLayout[player] == 'grid':
|
||||
parity[1] = 0
|
||||
parity[2] = 0
|
||||
if not world.owKeepSimilar[player]:
|
||||
parity[1] += 2*parity[2]
|
||||
parity[2] = 0
|
||||
@@ -1164,12 +1036,16 @@ def define_tile_groups(world, do_grouped, player):
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']:
|
||||
merge_groups([[0x05, 0x07]])
|
||||
|
||||
# all non-parallel screens
|
||||
if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped):
|
||||
merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]])
|
||||
# special screens
|
||||
if world.owLayout[player] != 'wild' and (world.owCrossed[player] == 'none' or do_grouped):
|
||||
merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81]])
|
||||
|
||||
# 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
|
||||
if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped):
|
||||
if world.owLayout[player] == 'wild' and world.owParallel[player] and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped):
|
||||
merge_groups([[0x28, 0x29]])
|
||||
|
||||
# whirlpool screens
|
||||
@@ -1225,7 +1101,7 @@ def reorganize_groups(world, groups, player):
|
||||
new_group[0] = None
|
||||
if world.owTerrain[player]:
|
||||
new_group[3] = None
|
||||
if world.owShuffle[player] != 'parallel':
|
||||
if not world.owParallel[player]:
|
||||
new_group[4] = None
|
||||
if not world.owKeepSimilar[player]:
|
||||
new_group[5] = None
|
||||
@@ -1294,19 +1170,6 @@ def adjust_edge_groups(world, trimmed_groups, edges_to_swap, player):
|
||||
groups[(mode, wrld, dir, terrain, parallel, count, group_name)][i].extend(matches)
|
||||
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):
|
||||
if from_region in mirror_connections and to_region in mirror_connections[from_region]:
|
||||
if len(mirror_connections[from_region]) == 1:
|
||||
@@ -1370,8 +1233,8 @@ def update_world_regions(world, player):
|
||||
world.get_region(name, player).type = RegionType.LightWorld
|
||||
|
||||
def can_reach_smith(world, player):
|
||||
from Items import ItemFactory
|
||||
from BaseClasses import CollectionState
|
||||
from Items import ItemFactory
|
||||
|
||||
def explore_region(region_name, region=None):
|
||||
nonlocal found
|
||||
@@ -1426,7 +1289,7 @@ def can_reach_smith(world, player):
|
||||
def build_sectors(world, player):
|
||||
from Main import copy_world_premature
|
||||
from OWEdges import OWTileRegions
|
||||
|
||||
|
||||
# perform accessibility check on duplicate world
|
||||
for p in range(1, world.players + 1):
|
||||
world.key_logic[p] = {}
|
||||
@@ -1478,8 +1341,8 @@ def build_sectors(world, player):
|
||||
|
||||
def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges=False, restrictive_follower=False):
|
||||
from BaseClasses import CollectionState
|
||||
from Main import copy_world_premature
|
||||
from Items import ItemFactory
|
||||
from Main import copy_world_premature
|
||||
from Utils import stack_size3a
|
||||
|
||||
def explore_region(region_name, region=None):
|
||||
@@ -1523,9 +1386,6 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F
|
||||
return explored_regions
|
||||
|
||||
def validate_layout(world, player):
|
||||
if world.accessibility[player] == 'none':
|
||||
return True
|
||||
|
||||
entrance_connectors = {
|
||||
'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'],
|
||||
'Kakariko Suburb Area': ['Maze Race Ledge'],
|
||||
@@ -1552,7 +1412,14 @@ def validate_layout(world, player):
|
||||
}
|
||||
|
||||
# TODO: Find a better source for the below lists, original sourced was deprecated
|
||||
from source.overworld.EntranceData import default_dungeon_connections, default_connector_connections, default_item_connections, default_shop_connections, default_drop_connections, default_dropexit_connections
|
||||
from source.overworld.EntranceData import (
|
||||
default_connector_connections,
|
||||
default_drop_connections,
|
||||
default_dropexit_connections,
|
||||
default_dungeon_connections,
|
||||
default_item_connections,
|
||||
default_shop_connections,
|
||||
)
|
||||
|
||||
dungeon_entrances = list(zip(*default_dungeon_connections + [('Ganons Tower', '')]))[0]
|
||||
connector_entrances = list(zip(*default_connector_connections))[0]
|
||||
@@ -1612,7 +1479,7 @@ def validate_layout(world, player):
|
||||
while unreachable_count != len(unreachable_regions):
|
||||
# find unreachable regions
|
||||
unreachable_regions = {}
|
||||
for region_name in list(OWTileRegions.copy().keys()):
|
||||
for region_name in list(OWTileRegions.keys()):
|
||||
if region_name not in explored_regions and region_name not in isolated_regions:
|
||||
region = world.get_region(region_name, player)
|
||||
unreachable_regions[region_name] = region
|
||||
@@ -1655,9 +1522,55 @@ def validate_layout(world, player):
|
||||
|
||||
if len(unreachable_regions):
|
||||
return False
|
||||
|
||||
|
||||
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 = [
|
||||
#('Links House ES', 'Octoballoon WS'),
|
||||
#('Links House NE', 'Lost Woods Pass SW')
|
||||
@@ -1871,10 +1784,6 @@ default_whirlpool_connections = [
|
||||
((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')),
|
||||
((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 = {
|
||||
@@ -2327,6 +2236,11 @@ parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb 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
|
||||
default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
|
||||
('Lost Woods SW', 'Lost Woods Pass NW'),
|
||||
@@ -2537,49 +2451,65 @@ isolated_regions = [
|
||||
'Pyramid Water'
|
||||
]
|
||||
|
||||
flute_data = {
|
||||
#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
|
||||
0x00: (['Lost Woods East Area', 'Skull Woods Forest'], 0x09, 0x1042, 0x022e, 0x0202, 0x0290, 0x0288, 0x029b, 0x028f, 0xfff2, 0x000e, 0x0290, 0x0288, 0x0290, 0x0290),
|
||||
0x02: (['Lumberjack Area', 'Dark Lumberjack Area'], 0x02, 0x059c, 0x00d6, 0x04e6, 0x0138, 0x0558, 0x0143, 0x0563, 0xfffa, 0xfffa, 0x0138, 0x0550),
|
||||
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),
|
||||
0x05: (['East Death Mountain (Bottom)', 'East Dark Death Mountain (Bottom)'], 0x0e, 0x1860, 0x031e, 0x0d00, 0x0388, 0x0da8, 0x038d, 0x0d7d, 0x0000, 0x0000, 0x0388, 0x0da8),
|
||||
0x07: (['Death Mountain TR Pegs Area', 'Turtle Rock Area'], 0x07, 0x0804, 0x0102, 0x0e1a, 0x0160, 0x0e90, 0x016f, 0x0e97, 0xfffe, 0x0006, 0x0160, 0x0f20),
|
||||
0x0a: (['Mountain Pass Area', 'Bumper Cave Area'], 0x0a, 0x0180, 0x0220, 0x0406, 0x0280, 0x0488, 0x028f, 0x0493, 0x0000, 0xfffa, 0x0280, 0x0488),
|
||||
0x0f: (['Zora Waterfall Area', 'Catfish Area'], 0x0f, 0x0316, 0x025c, 0x0eb2, 0x02c0, 0x0f28, 0x02cb, 0x0f2f, 0x0002, 0xfffe, 0x02d0, 0x0f38),
|
||||
0x10: (['Lost Woods Pass West Area', 'Skull Woods Pass West Area'], 0x10, 0x0080, 0x0400, 0x0000, 0x0448, 0x0058, 0x046f, 0x0085, 0x0000, 0x0000, 0x0448, 0x0058),
|
||||
0x11: (['Kakariko Fortune Area', 'Dark Fortune Area'], 0x11, 0x0912, 0x051e, 0x0292, 0x0588, 0x0318, 0x058d, 0x031f, 0x0000, 0xfffe, 0x0588, 0x0318),
|
||||
0x12: (['Kakariko Pond Area', 'Outcast Pond Area'], 0x12, 0x0890, 0x051a, 0x0476, 0x0578, 0x04f8, 0x0587, 0x0503, 0xfff6, 0x000a, 0x0578, 0x04f8),
|
||||
0x13: (['Sanctuary Area', 'Dark Chapel Area'], 0x13, 0x051c, 0x04aa, 0x06de, 0x0508, 0x0758, 0x0517, 0x0763, 0xfff6, 0x0002, 0x0508, 0x0758),
|
||||
0x14: (['Graveyard Area', 'Dark Graveyard Area'], 0x14, 0x089c, 0x051e, 0x08e6, 0x0580, 0x0958, 0x058b, 0x0963, 0x0000, 0xfffa, 0x0580, 0x0928, 0x0580, 0x0948),
|
||||
0x15: (['River Bend East Bank', 'Qirn Jump East Bank'], 0x15, 0x041a, 0x0486, 0x0ad2, 0x04e8, 0x0b48, 0x04f3, 0x0b4f, 0x0008, 0xfffe, 0x04f8, 0x0b60),
|
||||
0x16: (['Potion Shop Area', 'Dark Witch Area'], 0x16, 0x0888, 0x0516, 0x0c4e, 0x0578, 0x0cc8, 0x0583, 0x0cd3, 0xfffa, 0xfff2, 0x0598, 0x0ccf),
|
||||
0x17: (['Zora Approach Ledge', 'Catfish Approach Ledge'], 0x17, 0x039e, 0x047e, 0x0ef2, 0x04e0, 0x0f68, 0x04eb, 0x0f6f, 0x0000, 0xfffe, 0x04e0, 0x0f68),
|
||||
0x18: (['Kakariko Village', 'Village of Outcasts'], 0x18, 0x0b30, 0x0759, 0x017e, 0x07b7, 0x0200, 0x07c6, 0x020b, 0x0007, 0x0002, 0x07c0, 0x0210, 0x07c8, 0x01f8),
|
||||
0x1a: (['Forgotten Forest Area', 'Shield Shop Fence'], 0x1a, 0x081a, 0x070f, 0x04d2, 0x0770, 0x0548, 0x077c, 0x054f, 0xffff, 0xfffe, 0x0770, 0x0548),
|
||||
0x1b: (['Hyrule Castle Courtyard', 'Pyramid Area'], 0x1b, 0x0c30, 0x077a, 0x0786, 0x07d8, 0x07f8, 0x07e7, 0x0803, 0x0006, 0xfffa, 0x07d8, 0x07f8),
|
||||
0x1d: (['Wooden Bridge Area', 'Broken Bridge Northeast'], 0x1d, 0x0602, 0x06c2, 0x0a0e, 0x0720, 0x0a80, 0x072f, 0x0a8b, 0xfffe, 0x0002, 0x0720, 0x0a80),
|
||||
0x1e: (['Eastern Palace Area', 'Palace of Darkness Area'], 0x26, 0x1802, 0x091e, 0x0c0e, 0x09c0, 0x0c80, 0x098b, 0x0c8b, 0x0000, 0x0002, 0x09c0, 0x0c80),
|
||||
0x22: (['Blacksmith Area', 'Hammer Pegs Area'], 0x22, 0x058c, 0x08aa, 0x0462, 0x0908, 0x04d8, 0x0917, 0x04df, 0x0006, 0xfffe, 0x0908, 0x04d8),
|
||||
0x25: (['Sand Dunes Area', 'Dark Dunes Area'], 0x25, 0x030e, 0x085a, 0x0a76, 0x08b8, 0x0ae8, 0x08c7, 0x0af3, 0x0006, 0xfffa, 0x08b8, 0x0b08),
|
||||
0x28: (['Maze Race Area', 'Dig Game Area'], 0x28, 0x0908, 0x0b1e, 0x003a, 0x0b88, 0x00b8, 0x0b8d, 0x00bf, 0x0000, 0x0006, 0x0b88, 0x00b8),
|
||||
0x29: (['Kakariko Suburb Area', 'Frog Area'], 0x29, 0x0408, 0x0a7c, 0x0242, 0x0ae0, 0x02c0, 0x0aeb, 0x02c7, 0x0002, 0xfffe, 0x0ae0, 0x02c0),
|
||||
0x2a: (['Flute Boy Area', 'Stumpy Area'], 0x2a, 0x058e, 0x0aac, 0x046e, 0x0b10, 0x04e8, 0x0b1b, 0x04f3, 0x0002, 0x0002, 0x0b10, 0x04e8),
|
||||
0x2b: (['Central Bonk Rocks Area', 'Dark Bonk Rocks Area'], 0x2b, 0x0620, 0x0acc, 0x0700, 0x0b30, 0x0790, 0x0b3b, 0x0785, 0xfff2, 0x0000, 0x0b30, 0x0770),
|
||||
0x2c: (['Links House Area', 'Big Bomb Shop Area'], 0x2c, 0x0588, 0x0ab9, 0x0840, 0x0b17, 0x08b8, 0x0b26, 0x08bf, 0xfff7, 0x0000, 0x0b20, 0x08b8),
|
||||
0x2d: (['Stone Bridge South Area', 'Hammer Bridge South Area'], 0x2d, 0x0886, 0x0b1e, 0x0a2a, 0x0ba0, 0x0aa8, 0x0b8b, 0x0aaf, 0x0000, 0x0006, 0x0bc4, 0x0ad0),
|
||||
0x2e: (['Tree Line Area', 'Dark Tree Line Area'], 0x2e, 0x0100, 0x0a1a, 0x0c00, 0x0a78, 0x0c30, 0x0a87, 0x0c7d, 0x0006, 0x0000, 0x0a78, 0x0c58),
|
||||
0x2f: (['Eastern Nook Area', 'Darkness Nook Area'], 0x2f, 0x0798, 0x0afa, 0x0eb2, 0x0b58, 0x0f30, 0x0b67, 0x0f37, 0xfff6, 0x000e, 0x0b50, 0x0f30),
|
||||
0x30: (['Desert Teleporter Ledge', 'Mire Teleporter Ledge'], 0x38, 0x1880, 0x0f1e, 0x0000, 0x0fa8, 0x0078, 0x0f8d, 0x008d, 0x0000, 0x0000, 0x0fb0, 0x0070),
|
||||
0x32: (['Flute Boy Approach Area', 'Stumpy Approach Area'], 0x32, 0x03a0, 0x0c6c, 0x0500, 0x0cd0, 0x05a8, 0x0cdb, 0x0585, 0x0002, 0x0000, 0x0cd6, 0x0568),
|
||||
0x33: (['C Whirlpool Outer Area', 'Dark C Whirlpool Outer Area'], 0x33, 0x0180, 0x0c20, 0x0600, 0x0c80, 0x0628, 0x0c8f, 0x067d, 0x0000, 0x0000, 0x0c80, 0x0628),
|
||||
0x34: (['Statues Area', 'Hype Cave Area'], 0x34, 0x088e, 0x0d00, 0x0866, 0x0d60, 0x08d8, 0x0d6f, 0x08e3, 0x0000, 0x000a, 0x0d60, 0x08d8),
|
||||
#0x35: (['Lake Hylia Northwest Bank', 'Ice Lake Northwest Bank'], 0x35, 0x0d00, 0x0da6, 0x0a06, 0x0e08, 0x0a80, 0x0e13, 0x0a8b, 0xfffa, 0xfffa, 0x0d88, 0x0a88),
|
||||
0x35: (['Lake Hylia South Shore', 'Ice Lake Southeast Ledge'], 0x3e, 0x1860, 0x0f1e, 0x0d00, 0x0f98, 0x0da8, 0x0f8b, 0x0d85, 0x0000, 0x0000, 0x0f90, 0x0da4),
|
||||
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),
|
||||
0x3b: (['Dam Area', 'Swamp Area'], 0x3b, 0x069e, 0x0edf, 0x06f2, 0x0f3d, 0x0778, 0x0f4c, 0x077f, 0xfff1, 0xfffe, 0x0f30, 0x0770),
|
||||
0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0f38, 0x0898),
|
||||
0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0f80, 0x0ef0)
|
||||
ow_loc_prize_table = {
|
||||
'Master Sword Pedestal': (0x06d, 0x070),
|
||||
'Hobo': (0xb80, 0xb90),
|
||||
'Mushroom': (0x180, 0x140),
|
||||
'Lost Woods Hideout Tree': (0x200, 0x320),
|
||||
'Ether Tablet': (0x728, 0x017),
|
||||
'Spectacle Rock': (0x7F8, 0x128),
|
||||
'Old Man': (0x6A8, 0x3FF),
|
||||
'Floating Island': (0xD38, 0x038),
|
||||
'Death Mountain Bonk Rocks': (0xD48, 0x107),
|
||||
'Mountain Pass Pull Tree': (0x4E0, 0x380),
|
||||
'Mountain Pass Southeast Tree': (0x5A0, 0x3FF),
|
||||
'Lost Woods Pass West Tree': (0x080, 0x540),
|
||||
'Kakariko Portal Tree': (0x190, 0x540),
|
||||
'Fortune Bonk Rocks': (0x288, 0x558),
|
||||
'Bonk Rocks Tree': (0x680, 0x520),
|
||||
'Sanctuary Tree': (0x788, 0x594),
|
||||
'River Bend West Tree': (0xA38, 0x5FF),
|
||||
'River Bend East Tree': (0xB88, 0x558),
|
||||
'Bottle Merchant': (0x178, 0x7E8),
|
||||
'Blinds Hideout Tree': (0x118, 0x6F7),
|
||||
'Kakariko Welcome Tree': (0x358, 0x988),
|
||||
'Hyrule Castle Tree': (0x730, 0x780),
|
||||
'Wooden Bridge Tree': (0xA78, 0x6F0),
|
||||
'Eastern Palace Tree': (0xEC0, 0x968),
|
||||
'Maze Race': (0x0C0, 0xB00),
|
||||
'Flute Spot': (0x480, 0xAC0),
|
||||
'Flute Boy East Tree': (0x540, 0xB80),
|
||||
'Flute Boy South Tree': (0x540, 0xB80),
|
||||
'Central Bonk Rocks Tree': (0x768, 0xB37),
|
||||
'Tree Line Tree 2': (0xCD8, 0xB00),
|
||||
'Tree Line Tree 4': (0xCD8, 0xB00),
|
||||
'Desert Ledge': (0x018, 0xE88),
|
||||
'Bombos Tablet': (0x3E8, 0xEF8),
|
||||
'Flute Boy Approach North Tree': (0x588, 0xD07),
|
||||
'Flute Boy Approach South Tree': (0x588, 0xD07),
|
||||
'Lake Hylia Island': (0xB98, 0xD88),
|
||||
'Purple Chest': (0x588, 0xE96),
|
||||
'Sunken Treasure': (0x6F8, 0xF48),
|
||||
|
||||
'Dark Lumberjack Tree': (0x438, 0x1F7),
|
||||
'Bumper Cave Ledge': (0x538, 0x277),
|
||||
'Catfish': (0xE80, 0x368),
|
||||
'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 = \
|
||||
@@ -2601,23 +2531,3 @@ H(38)| sss 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|
|
||||
+---+-+-+-+---+-+"""
|
||||
|
||||
+33
-1
@@ -1,10 +1,42 @@
|
||||
# Past Feature Notes
|
||||
|
||||
1.4.3: File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!)
|
||||
1.5.0
|
||||
* 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
|
||||
|
||||
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
|
||||
* Rom fixes (all thanks to Codemann, I believe)
|
||||
* Pot bug when at sprite limit
|
||||
|
||||
@@ -3,28 +3,39 @@ import argparse
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import RaceRandom as random
|
||||
import time
|
||||
import sys
|
||||
import time
|
||||
|
||||
import RaceRandom as random
|
||||
from BaseClasses import World
|
||||
from Regions import create_regions
|
||||
from OverworldShuffle import link_overworld
|
||||
from source.overworld.EntranceShuffle2 import link_entrances_new, connect_entrance, connect_two_way, connect_exit
|
||||
from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons
|
||||
from Items import ItemFactory
|
||||
from ItemList import difficulties
|
||||
from Items import ItemFactory
|
||||
from Main import create_playthrough
|
||||
from OverworldShuffle import link_overworld
|
||||
from Regions import create_regions
|
||||
from Rom import (
|
||||
LocalRom,
|
||||
apply_rom_settings,
|
||||
get_sprite_from_name,
|
||||
patch_rom,
|
||||
write_string_to_rom,
|
||||
)
|
||||
from Rules import set_rules
|
||||
from source.overworld.EntranceShuffle2 import (
|
||||
connect_entrance,
|
||||
connect_exit,
|
||||
connect_two_way,
|
||||
link_entrances_new,
|
||||
)
|
||||
|
||||
__version__ = '0.2-dev'
|
||||
PlandoVersion = '0.2-dev'
|
||||
|
||||
def main(args):
|
||||
start_time = time.process_time()
|
||||
|
||||
# initialize the world
|
||||
world = World(1, 'vanilla', 'vanilla', 'vanilla', 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
||||
world = World(1, 'vanilla', True, '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")
|
||||
logger = logging.getLogger('')
|
||||
|
||||
@@ -36,7 +47,7 @@ def main(args):
|
||||
|
||||
random.seed(world.seed)
|
||||
|
||||
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
|
||||
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', PlandoVersion, args.plando)
|
||||
|
||||
world.difficulty_requirements[1] = difficulties[world.difficulty[1]]
|
||||
|
||||
@@ -157,9 +168,9 @@ def prefill_world(world, plando, text_patches):
|
||||
elif line.startswith('!goal'):
|
||||
_, goalstr = line.split(':', 1)
|
||||
world.goal = {1: goalstr.strip()}
|
||||
elif line.startswith('!owShuffle'):
|
||||
elif line.startswith('!owLayout'):
|
||||
_, modestr = line.split(':', 1)
|
||||
world.owShuffle = {1: modestr.strip()}
|
||||
world.owLayout = {1: modestr.strip()}
|
||||
elif line.startswith('!owCrossed'):
|
||||
_, modestr = line.split(':', 1)
|
||||
world.owCrossed = {1: modestr.strip()}
|
||||
|
||||
@@ -244,7 +244,7 @@ Ganons Tower - Validation Chest: Nothing
|
||||
Ganon: Triforce
|
||||
|
||||
# set Overworld connections (lines starting with $, separate edges with =)
|
||||
!owShuffle: parallel
|
||||
!owLayout: wild
|
||||
#!owMixed: true # Mixed OW not supported yet
|
||||
!owCrossed: none
|
||||
!owKeepSimilar: true
|
||||
|
||||
+3
-5
@@ -1,11 +1,9 @@
|
||||
import RaceRandom as random
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from BaseClasses import PotItem, Pot, PotFlags, CrystalBarrier, LocationType, RegionType
|
||||
from Utils import int16_as_bytes, pc_to_snes, snes_to_pc
|
||||
|
||||
import RaceRandom as random
|
||||
from BaseClasses import CrystalBarrier, LocationType, Pot, PotFlags, PotItem, RegionType
|
||||
from source.dungeon.RoomObject import RoomObject, Shuffled_Pot
|
||||
from Utils import int16_as_bytes, pc_to_snes, snes_to_pc
|
||||
|
||||
movable_switch_rooms = defaultdict(lambda: [],
|
||||
{'PoD Stalfos Basement': ['PoD Basement Ledge'],
|
||||
|
||||
@@ -132,20 +132,24 @@ 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)
|
||||
|
||||
## Overworld Layout Shuffle (--ow_shuffle)
|
||||
## Overworld Layout Shuffle (--ow_layout)
|
||||
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
|
||||
|
||||
OW Transitions are not shuffled.
|
||||
|
||||
### Parallel
|
||||
### Grid
|
||||
|
||||
OW Transitions are shuffled, but both worlds will have a matching layout, similar to that of vanilla.
|
||||
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.
|
||||
|
||||
### Full
|
||||
### Wild
|
||||
|
||||
OW Transitions are shuffled within each world separately.
|
||||
OW Transitions are shuffled with no respect to geometric coherence.
|
||||
|
||||
## Parallel (--ow_unparallel to disable)
|
||||
|
||||
With OW Layout Shuffle, this forces both worlds to have a matching layout.
|
||||
|
||||
## Free Terrain (--ow_terrain)
|
||||
|
||||
@@ -389,11 +393,17 @@ Districts are a concept originally conceived by Aerinon in the Door Randomizer,
|
||||
Show the help message and exit.
|
||||
|
||||
```
|
||||
--ow_shuffle <mode>
|
||||
--ow_layout <mode>
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
@@ -424,6 +434,12 @@ This gives each OW tile a random chance to be flipped to the opposite world
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
+7
-16
@@ -141,19 +141,10 @@ These are now independent of retro mode and have three options: None, Random, an
|
||||
|
||||
# Patch Notes
|
||||
|
||||
* 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)
|
||||
* 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
|
||||
* 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.5.6
|
||||
* Enemy Drops: Pikit are no longer eligible for dropped items due to a vanilla bug where a failed steal can overwrite the assigned drop.
|
||||
* Standard Mode: Reworked spawn refills to be more generous for enemy drop modes.
|
||||
* Customizer: Added `item_pool_adjust` section to apply additive/subtractive deltas to the base item pool rather than replacing it entirely.
|
||||
* 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.
|
||||
|
||||
|
||||
|
||||
+15
-4
@@ -1,9 +1,20 @@
|
||||
import collections
|
||||
from Items import ItemFactory
|
||||
from BaseClasses import Region, Location, Entrance, RegionType, Terrain, Shop, ShopType, LocationType, PotItem, PotFlags
|
||||
from PotShuffle import key_drop_data, vanilla_pots, choose_pots, PotSecretTable
|
||||
|
||||
from source.dungeon.EnemyList import setup_enemy_locations, enemy_names
|
||||
from BaseClasses import (
|
||||
Entrance,
|
||||
Location,
|
||||
LocationType,
|
||||
PotFlags,
|
||||
PotItem,
|
||||
Region,
|
||||
RegionType,
|
||||
Shop,
|
||||
ShopType,
|
||||
Terrain,
|
||||
)
|
||||
from Items import ItemFactory
|
||||
from PotShuffle import PotSecretTable, choose_pots, key_drop_data, vanilla_pots
|
||||
from source.dungeon.EnemyList import enemy_names, setup_enemy_locations
|
||||
|
||||
|
||||
def create_regions(world, player):
|
||||
|
||||
@@ -1,50 +1,91 @@
|
||||
import bisect
|
||||
import collections
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import Items
|
||||
import RaceRandom as random
|
||||
import struct
|
||||
import sys
|
||||
|
||||
try:
|
||||
import bps.apply
|
||||
import bps.io
|
||||
except ImportError:
|
||||
raise Exception('Could not load BPS module')
|
||||
|
||||
from BaseClasses import ShopType, Region, Location, OWEdge, Door, DoorType, RegionType, LocationType
|
||||
from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals
|
||||
from Dungeons import dungeon_music_addresses, dungeon_table
|
||||
from Regions import location_table, shop_to_location_table, retro_shops
|
||||
from RoomData import DoorKind
|
||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
||||
from Text import Uncle_texts, Ganon1_texts, Ganon_Phase_3_No_Silvers_texts, Ganon_Phase_3_No_Weakness_texts, TavernMan_texts, Sahasrahla2_texts
|
||||
from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
|
||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts
|
||||
from Text import LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts
|
||||
from Text import Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc
|
||||
from Items import ItemFactory, prize_item_table
|
||||
from source.overworld.EntranceData import door_addresses, ow_prize_table
|
||||
from source.overworld.EntranceShuffle2 import exit_ids
|
||||
from OverworldShuffle import default_flute_connections, flute_data
|
||||
from InitialSram import InitialSram
|
||||
from BaseClasses import (
|
||||
Door,
|
||||
DoorType,
|
||||
Location,
|
||||
LocationType,
|
||||
OWEdge,
|
||||
Region,
|
||||
RegionType,
|
||||
ShopType,
|
||||
)
|
||||
from DamageTable import DamageTable
|
||||
|
||||
from source.classes.SFX import randomize_sfx, randomize_sfxinstruments, randomize_songinstruments
|
||||
from source.item.FillUtil import valid_pot_items
|
||||
from DoorShuffle import DROptions, boss_indicator, compass_data, dungeon_portals
|
||||
from Dungeons import dungeon_music_addresses, dungeon_table
|
||||
from InitialSram import InitialSram
|
||||
from Items import ItemFactory, prize_item_table
|
||||
from OverworldShuffle import default_flute_connections, flute_data
|
||||
from Regions import location_table, retro_shops, shop_to_location_table
|
||||
from RoomData import DoorKind
|
||||
from source.classes.SFX import (
|
||||
randomize_sfx,
|
||||
randomize_sfxinstruments,
|
||||
randomize_songinstruments,
|
||||
)
|
||||
from source.dungeon.EnemyList import EnemySprite, setup_enemy_dungeon_tables
|
||||
from source.dungeon.RoomObject import DoorObject
|
||||
from source.enemizer.Bossmizer import boss_writes
|
||||
from source.enemizer.Enemizer import write_enemy_shuffle_settings
|
||||
|
||||
from source.item.FillUtil import valid_pot_items
|
||||
from source.overworld.EntranceData import door_addresses, ow_prize_table
|
||||
from source.overworld.EntranceShuffle2 import exit_ids
|
||||
from source.overworld.FluteShuffle import default_flute_connections, flute_data
|
||||
from Text import (
|
||||
Blacksmiths_texts,
|
||||
Blind_texts,
|
||||
BombShop2_texts,
|
||||
CompressedTextMapper,
|
||||
Credits,
|
||||
DeathMountain_texts,
|
||||
DesertPalace_texts,
|
||||
FluteBoy_texts,
|
||||
Ganon1_texts,
|
||||
Ganon_Phase_3_No_Silvers_texts,
|
||||
Ganon_Phase_3_No_Weakness_texts,
|
||||
Kakariko_texts,
|
||||
KingsReturn_texts,
|
||||
LinksHouse_texts,
|
||||
LostWoods_texts,
|
||||
Lumberjacks_texts,
|
||||
MagicShop_texts,
|
||||
MountainTower_texts,
|
||||
MultiByteTextMapper,
|
||||
Sahasrahla2_texts,
|
||||
Sahasrahla_names,
|
||||
Sanctuary_texts,
|
||||
SickKid_texts,
|
||||
TavernMan_texts,
|
||||
TextTable,
|
||||
Triforce_texts,
|
||||
Uncle_texts,
|
||||
WishingWell_texts,
|
||||
Zora_texts,
|
||||
junk_texts,
|
||||
text_addresses,
|
||||
)
|
||||
from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc
|
||||
from Versions import DRVersion, GKVersion, ORVersion
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '76dc2d00e5dd5b925ad01574b327d364'
|
||||
RANDOMIZERBASEHASH = '100c3e1da68680a0f3d8e1fc94568de6'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
@@ -162,11 +203,12 @@ class LocalRom(object):
|
||||
with open(local_path('data/base2current.bps'), 'rb') as stream:
|
||||
bps.apply.apply_to_bytearrays(bps.io.read_bps(stream), orig_buffer, self.buffer)
|
||||
|
||||
# verify md5
|
||||
patchedmd5 = hashlib.md5()
|
||||
patchedmd5.update(self.buffer)
|
||||
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
||||
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
if not os.getenv("SKIP_BASEROM_CHECK", False):
|
||||
# verify md5
|
||||
patchedmd5 = hashlib.md5()
|
||||
patchedmd5.update(self.buffer)
|
||||
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
||||
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
|
||||
def write_crc(self):
|
||||
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
@@ -489,14 +531,16 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
|
||||
# patch flute spots
|
||||
owFlags = 0
|
||||
if world.owFluteShuffle[player] == 'vanilla':
|
||||
owFog = 0
|
||||
if world.owFluteShuffle[player] == 'vanilla' and world.owLayout[player] != 'grid':
|
||||
flute_spots = default_flute_connections
|
||||
else:
|
||||
flute_spots = world.owflutespots[player]
|
||||
owFlags |= 0x0100
|
||||
write_int16(rom, snes_to_pc(0x0AB7F7), 0xEAEA)
|
||||
if world.owFluteShuffle[player] != 'vanilla':
|
||||
write_int16(rom, snes_to_pc(0x0AB80B), 0xEAEA)
|
||||
|
||||
flute_writes = sorted([(f, flute_data[f][1]) for f in flute_spots], key = lambda f: f[1])
|
||||
flute_writes = [(f, flute_data[f][1]) for f in flute_spots]
|
||||
for o in range(0, len(flute_writes)):
|
||||
owid = flute_writes[o][0]
|
||||
offset = 0
|
||||
@@ -509,46 +553,64 @@ 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(0x02E8F3 + (o * 2)), data[14] if offset > 0 and len(data) > 13 else data[6]) # link X
|
||||
|
||||
if offset == 0 or len(data) <= 15:
|
||||
write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[2]) # vram
|
||||
write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[3]) # BG scroll Y
|
||||
write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[4]) # BG scroll X
|
||||
write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[7]) # cam Y
|
||||
write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[8]) # cam X
|
||||
write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[9]) # unknown 1
|
||||
write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[10]) # unknown 2
|
||||
rom.write_byte(snes_to_pc(0x0AB783 + o), data[12] & 0xff) # flute menu blip - X low byte
|
||||
rom.write_byte(snes_to_pc(0x0AB78B + o), data[12] // 0x100) # flute menu blip - X high byte
|
||||
rom.write_byte(snes_to_pc(0x0AB793 + o), data[11] & 0xff) # flute menu blip - Y low byte
|
||||
rom.write_byte(snes_to_pc(0x0AB79B + o), data[11] // 0x100) # flute menu blip - Y high byte
|
||||
else: # use alternate flute data
|
||||
write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[15]) # vram
|
||||
write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[16]) # BG scroll Y
|
||||
write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[17]) # BG scroll X
|
||||
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
|
||||
base_index = 0
|
||||
if not (offset == 0 or len(data) <= 15):
|
||||
base_index = 13 # use alternate flute data
|
||||
write_int16(rom, snes_to_pc(0x02E86B + (o * 2)), data[base_index + 2]) # vram
|
||||
write_int16(rom, snes_to_pc(0x02E88D + (o * 2)), data[base_index + 3]) # BG scroll Y
|
||||
write_int16(rom, snes_to_pc(0x02E8AF + (o * 2)), data[base_index + 4]) # BG scroll X
|
||||
if base_index > 0:
|
||||
base_index -= 2
|
||||
write_int16(rom, snes_to_pc(0x02E915 + (o * 2)), data[base_index + 7]) # cam Y
|
||||
write_int16(rom, snes_to_pc(0x02E937 + (o * 2)), data[base_index + 8]) # cam X
|
||||
write_int16(rom, snes_to_pc(0x02E959 + (o * 2)), data[base_index + 9]) # unknown 1
|
||||
write_int16(rom, snes_to_pc(0x02E97B + (o * 2)), data[base_index + 10]) # unknown 2
|
||||
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, data[base_index + 12], data[base_index + 11], world.mode[player] == 'inverted')
|
||||
rom.write_byte(snes_to_pc(0x0AB783 + o), map_x & 0xff) # flute menu blip - X low byte
|
||||
rom.write_byte(snes_to_pc(0x0AB78B + o), map_x // 0x100) # flute menu blip - X high byte
|
||||
rom.write_byte(snes_to_pc(0x0AB793 + o), map_y & 0xff) # flute menu blip - Y low byte
|
||||
rom.write_byte(snes_to_pc(0x0AB79B + o), map_y // 0x100) # flute menu blip - Y high byte
|
||||
|
||||
# patch whirlpools
|
||||
if world.owWhirlpoolShuffle[player]:
|
||||
owFlags |= 0x01
|
||||
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
|
||||
inverted_buffer = [0] * 0x82
|
||||
owMode = 0
|
||||
if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]:
|
||||
if world.owShuffle[player] == 'parallel':
|
||||
owMode = 1
|
||||
elif world.owShuffle[player] == 'full':
|
||||
owMode = 2
|
||||
|
||||
if world.owKeepSimilar[player] and (world.owShuffle[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
|
||||
if world.owLayout[player] != 'vanilla' or world.owCrossed[player] not in ['none', 'polar'] or world.owMixed[player]:
|
||||
if world.owLayout[player] != 'vanilla':
|
||||
owMode = 1 if world.owParallel[player] else 2
|
||||
if world.owKeepSimilar[player] and (world.owLayout[player] != 'vanilla' or world.owCrossed[player] == 'unrestricted'):
|
||||
owMode |= 0x0100
|
||||
if world.owCrossed[player] != 'none' and (world.owCrossed[player] != 'polar' or world.owMixed[player]):
|
||||
owMode |= 0x0200
|
||||
@@ -583,10 +645,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
rom.write_byte(0x1539B0 + b + 9, world_flag)
|
||||
|
||||
for edge in world.owedges:
|
||||
if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player:
|
||||
if edge.player == player:
|
||||
write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc)
|
||||
if not edge.specialExit:
|
||||
rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget())
|
||||
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, destination)
|
||||
|
||||
# patch bonk prizes
|
||||
if world.shuffle_bonk_drops[player]:
|
||||
@@ -615,6 +678,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
|
||||
write_int16(rom, 0x150002, owMode)
|
||||
write_int16(rom, 0x150004, owFlags)
|
||||
write_int16(rom, 0x150008, owFog if world.owFog[player] else 0x00)
|
||||
|
||||
# patch entrance/exits/holes
|
||||
for region in world.regions:
|
||||
@@ -674,15 +738,15 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
if world.mirrorscroll[player] or world.doorShuffle[player] != 'vanilla':
|
||||
dr_flags |= DROptions.Town_Portal
|
||||
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']:
|
||||
dr_flags |= DROptions.Map_Info
|
||||
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
|
||||
rom.write_byte(snes_to_pc(0x308039), 1)
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
|
||||
and world.mixed_travel[player] == 'prevent':
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic' \
|
||||
and world.mixed_travel[player] == 'prevent':
|
||||
# PoD Falling Bridge or Hammjump
|
||||
# 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
|
||||
@@ -754,11 +818,13 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
rom.write_byte(0x138002, 2)
|
||||
for name, layout in world.key_layout[player].items():
|
||||
offset = compass_data[name][4]//2
|
||||
dungeon = world.get_dungeon(name, player)
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
rom.write_byte(0x187010+offset, layout.max_chests + layout.max_drops)
|
||||
else:
|
||||
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used
|
||||
rom.write_byte(0x187010+offset, layout.max_chests)
|
||||
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops + dungeon.extra_small_keys) # not currently used
|
||||
rom.write_byte(0x187010+offset, layout.max_chests + dungeon.extra_small_keys)
|
||||
rom.write_byte(0x187000+offset, dungeon.extra_small_keys)
|
||||
builder = world.dungeon_layouts[player][name]
|
||||
bk_status = 1 if builder.bk_required else 0
|
||||
bk_status = 2 if builder.bk_provided else bk_status
|
||||
@@ -780,9 +846,9 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
if world.doorShuffle[player] == 'basic':
|
||||
rom.write_byte(0x138002, 1)
|
||||
for door in world.doors:
|
||||
if door.dest is not None and isinstance(door.dest, Door) and\
|
||||
door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs,
|
||||
DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
||||
if door.dest is not None and isinstance(door.dest, Door) and \
|
||||
door.player == player and door.type in [DoorType.Normal, DoorType.SpiralStairs,
|
||||
DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
||||
rom.write_bytes(door.getAddress(), door.dest.getTarget(door))
|
||||
for paired_door in world.paired_doors[player]:
|
||||
rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player))
|
||||
@@ -928,7 +994,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
set_inverted_mode(world, player, rom, inverted_buffer)
|
||||
|
||||
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']:
|
||||
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']:
|
||||
# disable sword sprite from uncle
|
||||
rom.write_bytes(0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
|
||||
rom.write_bytes(0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
|
||||
@@ -1186,13 +1252,13 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
|
||||
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player]))
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforce pieces (instant win not enabled)
|
||||
|
||||
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
|
||||
|
||||
gametype = 0x04 # item
|
||||
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
|
||||
rom.write_byte(0x180211, gametype) # Game type
|
||||
|
||||
@@ -1215,7 +1281,6 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
rom.write_bytes(0x18016E, [0x04, 0x08, 0x10]) # Set spike cave and MM spike room Cape usage
|
||||
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
|
||||
rom.write_byte(0x50599, 0x00) # disable below ganon chest
|
||||
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
|
||||
if world.is_pyramid_open(player):
|
||||
rom.initial_sram.pre_open_pyramid_hole()
|
||||
rom.write_byte(0x18008F, 0x01 if world.is_atgt_swapped(player) else 0x00) # AT/GT swapped
|
||||
@@ -1261,6 +1326,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
# 08: Goal items collected (ie. Triforce Pieces)
|
||||
# 09: Max collection rate
|
||||
# 0A: Custom goal
|
||||
# 0B: Reserved for Bingo
|
||||
# 0C: All bosses (prize bosses + aga1 + aga2)
|
||||
|
||||
def get_goal_bytes(type):
|
||||
goal_bytes = []
|
||||
@@ -1275,8 +1342,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
else:
|
||||
goal_bytes += int16_as_bytes(req['target'])
|
||||
elif req['condition'] & 0x80 == 0:
|
||||
if req['condition'] & 0x7F == 0x06 or req['condition'] & 0x7F == 0x07:
|
||||
# agahnims have no target value
|
||||
if req['condition'] & 0x7F in [0x00, 0x06, 0x07]:
|
||||
# no target value
|
||||
pass
|
||||
elif req['condition'] & 0x7F < 0x08:
|
||||
goal_bytes += [req['target']]
|
||||
@@ -1315,6 +1382,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
ganon_goal += [0x02, world.crystals_needed_for_ganon[player]]
|
||||
elif world.goal[player] in ['ganonhunt']:
|
||||
ganon_goal += [0x88] # triforce pieces
|
||||
elif world.goal[player] in ['bosshunt']:
|
||||
if world.bosshunt_include_agas[player]:
|
||||
ganon_goal += [0x0C, world.bosses_ganon[player]] # total bosses
|
||||
else:
|
||||
ganon_goal += [0x05, world.bosses_ganon[player]] # prize bosses
|
||||
elif world.goal[player] in ['completionist']:
|
||||
ganon_goal += [0x81, 0x82, 0x06, 0x07, 0x89] # AD and max collection rate
|
||||
else:
|
||||
@@ -1412,9 +1484,82 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
| (0x04 if world.mapshuffle[player] != 'none' else 0x00)
|
||||
| (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
|
||||
if 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']):
|
||||
rom.write_byte(0x18003A, 0x01) # show key counts on map pickup
|
||||
map_hud_mode = 0x00
|
||||
if world.dungeon_counters[player] == 'on':
|
||||
map_hud_mode = 0x02 # always on
|
||||
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
|
||||
if world.prizeshuffle[player] != 'none':
|
||||
loot_source |= 0x10
|
||||
if world.pottery[player] not in ['none', 'cave']:
|
||||
loot_source |= 0x02
|
||||
if world.dropshuffle[player] != 'none':
|
||||
loot_source |= 0x04
|
||||
rom.write_byte(0x1CFF10, loot_source)
|
||||
|
||||
if world.loothud[player] == 'never':
|
||||
rom.write_byte(0x1CFF12, 0x00)
|
||||
elif world.loothud[player] == 'presence':
|
||||
rom.write_byte(0x1CFF12, 0x01)
|
||||
rom.write_bytes(0x1CFF0E, [0x01, 0x01])
|
||||
elif world.loothud[player] == 'value':
|
||||
rom.write_byte(0x1CFF12, 0x01)
|
||||
rom.write_bytes(0x1CFF0E, [0xFF, 0xFF])
|
||||
elif world.loothud[player] == 'dungeon_value':
|
||||
rom.write_byte(0x1CFF12, 0x01)
|
||||
rom.write_bytes(0x1CFF0E, [0xFF, 0x01])
|
||||
|
||||
if world.showloot[player] == 'never':
|
||||
rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00])
|
||||
rom.write_byte(0x1CFF11, 0x00)
|
||||
rom.write_byte(0x1CFF12, 0x00) # turn off hud icon too just to be safe
|
||||
elif world.showloot[player] == 'presence':
|
||||
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00])
|
||||
rom.write_byte(0x1CFF11, 0x00)
|
||||
elif world.showloot[player] == 'compass':
|
||||
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00])
|
||||
rom.write_byte(0x1CFF11, 0x01)
|
||||
elif world.showloot[player] == 'always':
|
||||
rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00])
|
||||
rom.write_byte(0x1CFF11, 0x00)
|
||||
|
||||
if world.showmap[player] == 'visited':
|
||||
rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05])
|
||||
elif world.showmap[player] == 'map':
|
||||
rom.write_bytes(0x1CFF00, [0x01, 0x05, 0x00, 0x05])
|
||||
elif world.showmap[player] == 'always':
|
||||
rom.write_bytes(0x1CFF00, [0x05, 0x00, 0x00, 0x00])
|
||||
|
||||
loot_icons = 0x1CF900
|
||||
if world.bombbag[player]:
|
||||
rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major
|
||||
|
||||
triforce_piece_ids = [0x6B, 0x6C]
|
||||
if world.treasure_hunt_count[player] > 20:
|
||||
for triforce_piece_id in triforce_piece_ids:
|
||||
rom.write_byte(loot_icons + triforce_piece_id, 0x04)
|
||||
|
||||
crystal_ids = [0x20, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6]
|
||||
if world.goal[player] in ['ganon', 'dungeons', 'crystals', 'trinity']:
|
||||
crystal_category = 0x0D
|
||||
else:
|
||||
crystal_category = 0x06
|
||||
for crystal_id in crystal_ids:
|
||||
rom.write_byte(loot_icons + crystal_id, crystal_category)
|
||||
|
||||
pendant_ids = [0x37, 0x38, 0x39]
|
||||
if world.goal[player] in ['pedestal', 'dungeons', 'trinity']:
|
||||
pendant_category = 0x0C
|
||||
else:
|
||||
pendant_category = 0x06
|
||||
for pendant_id in pendant_ids:
|
||||
rom.write_byte(loot_icons + pendant_id, pendant_category)
|
||||
|
||||
# compasses showing dungeon count
|
||||
compass_mode = 0x80 if world.compassshuffle[player] not in ['none', 'nearby'] else 0x00
|
||||
@@ -1434,36 +1579,53 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
rom.write_byte(0x18003C, compass_mode)
|
||||
|
||||
def get_entrance_coords(ent):
|
||||
if ent is None:
|
||||
owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47]
|
||||
x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0]
|
||||
y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0]
|
||||
world_indicator = 0x0000
|
||||
idx = int((map_index-2)/2)
|
||||
if ent is None or isinstance(ent, int):
|
||||
owid_map = [ 0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47 ]
|
||||
coord_flags = 0x0000
|
||||
if ent is None:
|
||||
# HUD-style dislocated icons
|
||||
x_map_position = [0x0050, 0x0080, 0xFF00, 0x0040, 0x0020, 0x00C0, 0x0060, 0x00A0, 0x00B0, 0x0080, 0x00E0]
|
||||
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]
|
||||
map_x = x_map_position[idx]
|
||||
map_y = y_map_position[idx]
|
||||
if owid != 0xFF:
|
||||
if (owid < 0x40) == (world.is_tile_swapped(owid, player)):
|
||||
world_indicator = 0x8000
|
||||
return [world_indicator | x_map_position_generic[idx], y_map_position_generic[idx]]
|
||||
if type(ent) is Location:
|
||||
from OverworldShuffle import OWTileRegions
|
||||
if ent.name == 'Hobo':
|
||||
coords = (0xb80, 0xb80)
|
||||
elif ent.name == 'Master Sword Pedestal':
|
||||
coords = (0x06d, 0x070)
|
||||
if (owid < 0x40) == (owid in world.owswaps[player][0]):
|
||||
coord_flags |= 0x8000 # world indicator flag
|
||||
if coord_flags & 0x4000 == 0:
|
||||
map_x, map_y = adjust_ow_coordinates_to_layout(world, player, map_x, map_y, coord_flags & 0x8000 != 0)
|
||||
return (coord_flags | map_x, map_y)
|
||||
elif type(ent) is Location:
|
||||
from OverworldShuffle import OWTileRegions, ow_loc_prize_table
|
||||
if ent.name in ow_loc_prize_table:
|
||||
coords = ow_loc_prize_table[ent.name]
|
||||
else:
|
||||
owid = OWTileRegions[ent.parent_region.name]
|
||||
if owid == 0x81:
|
||||
coords = (0x220, 0xf40)
|
||||
coords = (0xF60, 0x280)
|
||||
else:
|
||||
owid = owid % 0x40
|
||||
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)
|
||||
else:
|
||||
coords = ow_prize_table[ent.name]
|
||||
coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1])
|
||||
if ent.name in ow_prize_table:
|
||||
coords = ow_prize_table[ent.name]
|
||||
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
|
||||
|
||||
if world.overworld_map[player] == 'default':
|
||||
# disable HC/AT/GT icons
|
||||
if not world.owMixed[player]:
|
||||
@@ -1490,16 +1652,16 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
# prize location
|
||||
write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords)
|
||||
if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default':
|
||||
# TODO: I think this is logically the same as some of the vanilla stuff below
|
||||
vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)',
|
||||
'Desert Palace': 'Desert Palace Entrance (North)',
|
||||
'Skull Woods': 'Skull Woods Final Section' }
|
||||
entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon
|
||||
if world.is_atgt_swapped(player):
|
||||
swap_entrances = { 'Agahnims Tower': 'Ganons Tower',
|
||||
'Ganons Tower': 'Agahnims Tower' }
|
||||
entrance_name = swap_entrances[dungeon] if dungeon in swap_entrances else entrance_name
|
||||
entrance = world.get_entrance(entrance_name, player)
|
||||
swap_entrances = { 'Agahnims Tower': 'Ganons Tower',
|
||||
'Ganons Tower': 'Agahnims Tower' }
|
||||
if dungeon in swap_entrances:
|
||||
entrance_name = dungeon
|
||||
if world.is_atgt_swapped(player):
|
||||
entrance_name = swap_entrances[dungeon]
|
||||
entrance = world.get_entrance(entrance_name, player)
|
||||
coords = get_entrance_coords(entrance)
|
||||
else:
|
||||
coords = get_entrance_coords(int((map_index-2)/2))
|
||||
else:
|
||||
if len(portal_list) == 1:
|
||||
portal_idx = 0
|
||||
@@ -1519,8 +1681,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
offset += 4
|
||||
portal = world.get_portal(portal_list[portal_idx], player)
|
||||
entrance = portal.find_portal_entrance()
|
||||
coords = get_entrance_coords(entrance)
|
||||
|
||||
coords = get_entrance_coords(entrance)
|
||||
|
||||
# figure out compass entrances and what world (light/dark)
|
||||
write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords)
|
||||
if world.prizeshuffle[player] in ['none', 'dungeon', 'nearby'] and dungeon_table[dungeon].prize:
|
||||
@@ -1562,12 +1724,28 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
# b - Big Key
|
||||
# a - Small Key
|
||||
#
|
||||
dungeon_items_menu = 0x00
|
||||
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
dungeon_items_menu |= 0x0F
|
||||
|
||||
if world.keyshuffle[player] not in ['none', 'universal']:
|
||||
dungeon_items_menu |= 0x01
|
||||
|
||||
if world.bigkeyshuffle[player] != 'none':
|
||||
dungeon_items_menu |= 0x02
|
||||
|
||||
enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] not in ['none', 'dungeon', 'nearby']
|
||||
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00)
|
||||
| (0x02 if world.bigkeyshuffle[player] != 'none' else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] != 'none' or enable_menu_map_check else 0x00)
|
||||
| (0x08 if world.compassshuffle[player] != 'none' else 0x00) # free roaming items in menu
|
||||
| (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon
|
||||
if world.mapshuffle[player] != 'none' or enable_menu_map_check:
|
||||
dungeon_items_menu |= 0x04
|
||||
|
||||
if world.compassshuffle[player] != 'none':
|
||||
dungeon_items_menu |= 0x08
|
||||
|
||||
if world.logic[player] == 'nologic' or world.goal[player] == 'bosshunt':
|
||||
dungeon_items_menu |= 0x10
|
||||
|
||||
rom.write_byte(0x180045, dungeon_items_menu)
|
||||
|
||||
def get_reveal_bytes(itemName):
|
||||
if world.prizeshuffle[player] != 'wild':
|
||||
@@ -1601,53 +1779,40 @@ 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(0x18008B, 0x01 if glitches_enabled else 0x00)
|
||||
|
||||
# remove shield from uncle
|
||||
rom.write_bytes(0x6D253, [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(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E])
|
||||
rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E])
|
||||
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
|
||||
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
|
||||
if uncle_location.item is None or uncle_location.item.name not in ['Sword and Shield']:
|
||||
# remove shield from uncle
|
||||
rom.write_bytes(0x6D253, [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(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E])
|
||||
rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E])
|
||||
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 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.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']:
|
||||
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
|
||||
write_int16(rom, 0x180183, 300) # Escape fill rupee bow
|
||||
rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
bow_max, bow_small = 70, 10
|
||||
rom.write_byte(0x180187, 70) # Uncle respawn refill arrows
|
||||
rom.write_byte(0x18018A, 70) # Zelda respawn refill arrows
|
||||
rom.write_byte(0x18018D, 70) # Mantle respawn refill arrowss
|
||||
elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']:
|
||||
rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
|
||||
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
bomb_max = 50
|
||||
rom.write_byte(0x180186, 50) # Uncle respawn refill bombs
|
||||
rom.write_byte(0x180189, 50) # Zelda respawn refill bombs
|
||||
rom.write_byte(0x18018C, 50) # Mantle respawn refill bombs
|
||||
elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']:
|
||||
rom.write_byte(0x18004E, 4) # Escape Fill (magic)
|
||||
rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||
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)
|
||||
rom.write_byte(0x180185, 0x80) # Uncle respawn refill magic
|
||||
rom.write_byte(0x180188, 0x80) # Zelda respawn refill magic
|
||||
rom.write_byte(0x18018B, 0x80) # Mantle respawn refill magic
|
||||
|
||||
# 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)
|
||||
@@ -1750,7 +1915,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
write_enemy_shuffle_settings(world, player, rom)
|
||||
|
||||
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:
|
||||
if room.player == player and room.modified:
|
||||
if room.index in world.data_tables[player].room_list:
|
||||
@@ -1767,7 +1932,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
|
||||
write_enemizer_tweaks(rom, world, player)
|
||||
randomize_damage_table(rom, world, player)
|
||||
write_strings(rom, world, player, team)
|
||||
write_strings(rom, world, player, team, is_mystery)
|
||||
|
||||
# write initial sram
|
||||
rom.write_initial_sram()
|
||||
@@ -1776,30 +1941,29 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
|
||||
# set rom name
|
||||
# 21 bytes
|
||||
from Main import __version__
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
if rom_header:
|
||||
if len(rom_header) > 21:
|
||||
raise Exception('ROM header too long. Max 21 bytes, found %d bytes.' % len(rom_header))
|
||||
elif '|' in rom_header:
|
||||
gen, seedstring = rom_header.split('|', 1)
|
||||
gen = f'{gen:<3}'
|
||||
seedstring = f'{int(seedstring):09}' if seedstring.isdigit() else seedstring[:9]
|
||||
rom.name = bytearray(f'OR{gen}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21]
|
||||
elif len(rom_header) <= 9:
|
||||
seedstring = f'{int(rom_header):09}' if rom_header.isdigit() else rom_header
|
||||
rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21]
|
||||
else:
|
||||
rom.name = bytearray(rom_header, 'utf8')[:21]
|
||||
else:
|
||||
seedstring = f'{world.seed:09}' if isinstance(world.seed, int) else world.seed
|
||||
rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21]
|
||||
|
||||
if world.players > 1 and len(rom_header) <= 12:
|
||||
rom.name = bytearray(f"GK_{team + 1}_{player}_{rom_header}", 'utf8')
|
||||
elif len(rom_header) <= 18:
|
||||
rom.name = bytearray(f"GK_{rom_header}", 'utf8')
|
||||
else:
|
||||
rom.name = bytearray(rom_header, 'utf8')
|
||||
else:
|
||||
if world.players > 1:
|
||||
rom.name = bytearray(f'GK_{team + 1}_{player}_{world.seed}', 'utf8')
|
||||
else:
|
||||
rom.name = bytearray(f'GK_{world.seed}', 'utf8')
|
||||
|
||||
rom.name = rom.name[:21]
|
||||
rom.name.extend([0] * (21 - len(rom.name)))
|
||||
rom.write_bytes(0x7FC0, rom.name)
|
||||
|
||||
rom.write_bytes(0x138010, bytearray(__version__, 'utf8'))
|
||||
rom.write_bytes(0x138010, bytearray(DRVersion, 'utf8'))
|
||||
rom.write_bytes(0x150010, bytearray(ORVersion, 'utf8'))
|
||||
rom.write_bytes(0x1CEEF0, bytearray(GKVersion, 'utf8'))
|
||||
|
||||
# set player names
|
||||
for p in range(1, min(world.players, 255) + 1):
|
||||
@@ -1813,7 +1977,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
||||
(hashint >> 10) & 0x1F,
|
||||
(hashint >> 5) & 0x1F,
|
||||
hashint & 0x1F,
|
||||
]
|
||||
]
|
||||
rom.write_bytes(0x180215, code)
|
||||
rom.hash = code
|
||||
|
||||
@@ -1911,6 +2075,49 @@ def hud_format_text(text):
|
||||
output += b'\x7f\x00'
|
||||
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,
|
||||
ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx,
|
||||
@@ -2227,7 +2434,7 @@ def write_string_to_rom(rom, target, string):
|
||||
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||
|
||||
|
||||
def write_strings(rom, world, player, team):
|
||||
def write_strings(rom, world, player, team, is_mystery=False):
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
if world.shuffle[player] != 'vanilla':
|
||||
@@ -2235,6 +2442,9 @@ def write_strings(rom, world, player, team):
|
||||
" Crosskeys\n"
|
||||
" Tournament\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"
|
||||
" ~~~2021~~~\n Goomba\n\n"
|
||||
" ~~~2020~~~\n Linlinlin\n\n"
|
||||
@@ -2426,7 +2636,7 @@ def write_strings(rom, world, player, team):
|
||||
if world.is_tile_swapped(0x18, player) or world.flute_mode[player] in ['active', 'pseudo']:
|
||||
items_to_hint.remove(flute_item)
|
||||
flute_item = 'Ocarina (Activated)'
|
||||
if world.owShuffle[player] != 'vanilla' or world.owMixed[player]:
|
||||
if world.owLayout[player] != 'vanilla' or world.owMixed[player]:
|
||||
# Adding a guaranteed hint for the Flute in overworld shuffle.
|
||||
this_location = world.find_items_not_key_only(flute_item, player)
|
||||
if this_location and this_location not in hinted_locations:
|
||||
@@ -2444,7 +2654,7 @@ def write_strings(rom, world, player, team):
|
||||
random.shuffle(items_to_hint)
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8
|
||||
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
|
||||
hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
|
||||
hint_count += 1 if world.owLayout[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
|
||||
while hint_count > 0 and len(items_to_hint) > 0:
|
||||
this_item = items_to_hint.pop(0)
|
||||
this_location = world.find_items_not_key_only(this_item, player)
|
||||
@@ -2602,16 +2812,19 @@ def write_strings(rom, world, player, team):
|
||||
loc.item = i
|
||||
return loc
|
||||
(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']:
|
||||
(crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]])
|
||||
tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6)
|
||||
tt['bomb_shop'] = f'{bigbomb_follower}My supply is blocked until you clear %s and %s.' % (crystal5, crystal6)
|
||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant
|
||||
elif world.prizeshuffle[player] == 'nearby':
|
||||
(crystal5, crystal6, greenpendant) = tuple([x.item.dungeon_object.name for x in [crystal5, crystal6, greenpendant]])
|
||||
tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found near %s and %s.' % (crystal5, crystal6)
|
||||
tt['bomb_shop'] = f'{bigbomb_follower}The crystals can be found near %s and %s.' % (crystal5, crystal6)
|
||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom near %s' % greenpendant
|
||||
else:
|
||||
tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text)
|
||||
tt['bomb_shop'] = f'{bigbomb_follower}The 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['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]
|
||||
@@ -2672,13 +2885,19 @@ def write_strings(rom, world, player, team):
|
||||
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])
|
||||
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':
|
||||
bosshunt_count = '%d guardian%s of %sdungeons' % \
|
||||
(world.bosses_ganon[player],
|
||||
'' if world.bosses_ganon[player] == 1 else 's',
|
||||
'' if world.bosshunt_include_agas[player] else 'prize ')
|
||||
tt['sign_ganon'] = 'To beat Ganon you must defeat %s.' % bosshunt_count
|
||||
elif world.goal[player] == 'completionist':
|
||||
tt['sign_ganon'] = 'Ganon only respects those who have done everything'
|
||||
tt['sign_ganon'] = 'Ganon only respects those who have done everything.'
|
||||
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
|
||||
|
||||
def get_custom_goal_text(type):
|
||||
goal_text = world.custom_goals[player][type]['goaltext']
|
||||
placeholder_count = goal_text.count('%d')
|
||||
@@ -2730,7 +2949,22 @@ def write_strings(rom, world, player, team):
|
||||
sanc_text = "Dark Chapel"
|
||||
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}"
|
||||
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}\nYou must determine and achieve your goal."
|
||||
+ "{PAUSE9} {CHANGEPIC}", False)
|
||||
elif world.mode[player] == 'inverted':
|
||||
tt['intro_main'] = CompressedTextMapper.convert(
|
||||
"{INTRO}\n Episode III"
|
||||
+ "{PAUSE3}\n A Link to the Past"
|
||||
@@ -2744,6 +2978,7 @@ def write_strings(rom, world, player, team):
|
||||
+ "{PAUSE7}\nThis is your chance to be a hero."
|
||||
+ "{PAUSE3} {CHANGEPIC}\nYou must get the 7 crystals to beat Ganon."
|
||||
+ "{PAUSE9} {CHANGEPIC}", False)
|
||||
|
||||
rom.write_bytes(0xE0000, tt.getBytes())
|
||||
|
||||
credits = Credits()
|
||||
@@ -3072,6 +3307,13 @@ def update_compasses(rom, dungeon_locations, world, player):
|
||||
if not provided_dungeon:
|
||||
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',
|
||||
@@ -3345,7 +3587,10 @@ Prizes = ['Green Pendant',
|
||||
]
|
||||
|
||||
hash_alphabet = [
|
||||
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake",
|
||||
"Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
|
||||
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
|
||||
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder",
|
||||
"Ice Rod", "Green Pendant", "Bombos", "Ether", "Quake", "Lamp",
|
||||
"Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle",
|
||||
"Green Potion", "Somaria", "Cape", "Mirror", "Boots", "Gloves",
|
||||
"Flippers", "Pearl", "Shield", "Green Tunic", "Heart", "Map",
|
||||
"Compass", "Key",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from enum import Enum, unique
|
||||
|
||||
from Tables import door_pair_offset_table
|
||||
|
||||
|
||||
|
||||
@@ -3,18 +3,28 @@ import logging
|
||||
from collections import deque
|
||||
|
||||
import OverworldGlitchRules
|
||||
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType, LocationType, Terrain
|
||||
from BaseClasses import PotFlags
|
||||
from BaseClasses import (
|
||||
CollectionState,
|
||||
CrystalBarrier,
|
||||
DoorType,
|
||||
Entrance,
|
||||
KeyRuleType,
|
||||
LocationType,
|
||||
PotFlags,
|
||||
RegionType,
|
||||
Terrain,
|
||||
)
|
||||
from Dungeons import dungeon_table
|
||||
from RoomData import DoorKind
|
||||
from OWEdges import OWExitTypes
|
||||
from OverworldGlitchRules import overworld_glitches_rules
|
||||
from UnderworldGlitchRules import underworld_glitches_rules
|
||||
|
||||
from source.logic.Rule import RuleFactory
|
||||
from OWEdges import OWExitTypes
|
||||
from RoomData import DoorKind
|
||||
from source.dungeon.EnemyList import EnemySprite, Sprite
|
||||
from source.enemizer.EnemyLogic import special_rules_check, special_rules_for_region, defeat_rule_single
|
||||
from source.enemizer.EnemyLogic import defeat_rule_multiple, and_rule as and_rule_new, or_rule as or_rule_new
|
||||
from source.enemizer.EnemyLogic import and_rule as and_rule_new
|
||||
from source.enemizer.EnemyLogic import defeat_rule_multiple, defeat_rule_single
|
||||
from source.enemizer.EnemyLogic import or_rule as or_rule_new
|
||||
from source.enemizer.EnemyLogic import special_rules_check, special_rules_for_region
|
||||
from source.logic.Rule import RuleFactory
|
||||
from UnderworldGlitchRules import underworld_glitches_rules
|
||||
|
||||
|
||||
def set_rules(world, player):
|
||||
@@ -80,6 +90,15 @@ def set_rules(world, player):
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player))
|
||||
elif world.goal[player] == 'ganonhunt':
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
|
||||
elif world.goal[player] == 'bosshunt':
|
||||
if world.bosshunt_include_agas[player]:
|
||||
add_rule(world.get_location('Ganon', player), lambda state:
|
||||
state.item_count('Beat Agahnim 1', player) +
|
||||
state.item_count('Beat Agahnim 2', player) +
|
||||
state.item_count('Beat Boss', player) >= world.bosses_ganon[player])
|
||||
else:
|
||||
add_rule(world.get_location('Ganon', player), lambda state:
|
||||
state.item_count('Beat Boss', player) >= world.bosses_ganon[player])
|
||||
elif world.goal[player] == 'completionist':
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))
|
||||
|
||||
@@ -486,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 (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?
|
||||
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 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 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('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
@@ -1385,8 +1404,7 @@ 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 Second Section Hole', player), player)
|
||||
add_bunny_rule(world.get_entrance('East Dark Death Mountain Bushes', 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 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 (Inner)', player), player)
|
||||
add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player)
|
||||
@@ -1813,7 +1831,9 @@ def standard_rules(world, 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:
|
||||
add_rule(world.get_location('Hyrule Castle Courtyard Tree Pull', player), lambda state: state.has('Zelda Delivered', player))
|
||||
loc = world.get_location_unsafe('Hyrule Castle Courtyard Tree Pull', 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
|
||||
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))
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import multiprocessing
|
||||
import subprocess
|
||||
import sys
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
from collections import OrderedDict
|
||||
|
||||
cpu_threads = multiprocessing.cpu_count()
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import csv
|
||||
import multiprocessing
|
||||
import subprocess
|
||||
import sys
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
from collections import OrderedDict
|
||||
import csv
|
||||
|
||||
cpu_threads = multiprocessing.cpu_count()
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
text_addresses = {'Pedestal': (0x180300, 256),
|
||||
@@ -134,6 +135,7 @@ Triforce_texts = [
|
||||
" I promise the\n next seed will\n be better.",
|
||||
"\n Honk.",
|
||||
" Breakfast\n is served!",
|
||||
"\n send help",
|
||||
]
|
||||
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?']
|
||||
@@ -387,7 +389,6 @@ DeathMountain_texts = [
|
||||
]
|
||||
LostWoods_texts = [
|
||||
'thieves\' stump',
|
||||
'He\'s got wood',
|
||||
] * 2 + [
|
||||
"the forest thief",
|
||||
"dancing pickles",
|
||||
@@ -1780,7 +1781,7 @@ class TextTable(object):
|
||||
text['mastersword_pedestal_translated'] = CompressedTextMapper.convert("A test of strength: If you have 3 pendants, I'm yours.")
|
||||
text['telepathic_tile_spectacle_rock'] = CompressedTextMapper.convert("{NOBORDER}\n{NOBORDER}\nUse the Mirror, or the Hookshot and Hammer, to get to Tower of Hera!")
|
||||
text['telepathic_tile_swamp_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nDrain the floodgate to raise the water here!")
|
||||
text['telepathic_tile_thieves_town_upstairs'] = CompressedTextMapper.convert("{NOBORDER}\nBlind hate's bright light.")
|
||||
text['telepathic_tile_thieves_town_upstairs'] = CompressedTextMapper.convert("{NOBORDER}\nBlind hates bright light.")
|
||||
text['telepathic_tile_misery_mire'] = CompressedTextMapper.convert("{NOBORDER}\nLighting 4 torches will open your way forward!")
|
||||
text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^")
|
||||
text['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.")
|
||||
@@ -1800,6 +1801,7 @@ class TextTable(object):
|
||||
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['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n"
|
||||
" ~~~2025~~~\nGammachuu\n\n"
|
||||
" ~~~2024~~~\nGammachuu\n\n"
|
||||
" ~~~2023~~~\nGanonsGoneWild\n\n"
|
||||
" ~~~2022~~~\nObscure\n\n"
|
||||
@@ -2014,7 +2016,7 @@ class TextTable(object):
|
||||
text['thief_desert_rupee_cave'] = CompressedTextMapper.convert("So you, like, busted down my door, and are being a jerk by talking to me? Normally I would be angry and make you pay for it, but I bet you're just going to break all my pots and steal my 50 rupees.")
|
||||
text['thief_ice_rupee_cave'] = CompressedTextMapper.convert("I'm a rupee pot farmer. One day I will take over the world with my skillz. Have you met my brother in the desert? He's way richer than I am.")
|
||||
text['telepathic_tile_south_east_darkworld_cave'] = CompressedTextMapper.convert("~~ dev cave ~~\n no farming\n required")
|
||||
text['cukeman'] = CompressedTextMapper.convert("Did you hear that Veetorp beat ajneb174 in a 1 on 1 race at AGDQ?")
|
||||
text['cukeman'] = CompressedTextMapper.convert("Trans rights!")
|
||||
text['cukeman_2'] = CompressedTextMapper.convert("You found Shabadoo, huh?\nNiiiiice.")
|
||||
text['potion_shop_no_cash'] = CompressedTextMapper.convert("Yo! I'm not running a charity here.")
|
||||
text['kakariko_powdered_chicken'] = CompressedTextMapper.convert("Smallhacker…\n\n\nWas hiding, you found me!\n\n\nOkay, you can leave now.")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import functools
|
||||
from BaseClasses import Entrance, DoorType, Door
|
||||
from DoorShuffle import connect_simple_door
|
||||
|
||||
import Rules
|
||||
from BaseClasses import Door, DoorType, Entrance
|
||||
from DoorShuffle import connect_simple_door
|
||||
|
||||
kikiskip_spots = [
|
||||
("Kiki Skip", "Spectacle Rock Cave (Bottom)", "Palace of Darkness Portal")
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import fileinput
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import defaultdict
|
||||
from math import factorial
|
||||
from hashlib import md5
|
||||
from itertools import count
|
||||
import fileinput
|
||||
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import yaml
|
||||
from math import factorial
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def int16_as_bytes(value):
|
||||
value = value & 0xFFFF
|
||||
@@ -102,31 +103,13 @@ def close_console():
|
||||
pass
|
||||
|
||||
|
||||
def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', new_rom='working.sfc'):
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
import hashlib
|
||||
with open(old_rom, 'rb') as stream:
|
||||
old_rom_data = bytearray(stream.read())
|
||||
def get_new_romhash(new_rom='working.sfc'):
|
||||
with open(new_rom, 'rb') as stream:
|
||||
new_rom_data = bytearray(stream.read())
|
||||
# extend to 2 mb
|
||||
old_rom_data.extend(bytearray([0x00] * (2097152 - len(old_rom_data))))
|
||||
|
||||
out_data = OrderedDict()
|
||||
for idx, old in enumerate(old_rom_data):
|
||||
new = new_rom_data[idx]
|
||||
if old != new:
|
||||
out_data[idx] = [int(new)]
|
||||
for offset in reversed(list(out_data.keys())):
|
||||
if offset - 1 in out_data:
|
||||
out_data[offset-1].extend(out_data.pop(offset))
|
||||
with open('data/base2current.json', 'wt') as outfile:
|
||||
json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":"))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5 = md5()
|
||||
basemd5.update(new_rom_data)
|
||||
return "New Rom Hash: " + basemd5.hexdigest()
|
||||
return basemd5.hexdigest()
|
||||
|
||||
|
||||
def kth_combination(k, l, r):
|
||||
@@ -373,6 +356,26 @@ def update_deprecated_args(args):
|
||||
else:
|
||||
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
|
||||
# Don't do: Yes
|
||||
# Do: No
|
||||
@@ -768,18 +771,18 @@ class bidict(dict):
|
||||
super(bidict, self).__init__(*args, **kwargs)
|
||||
self.inverse = {}
|
||||
for key, value in self.items():
|
||||
self.inverse.setdefault(value,[]).append(key)
|
||||
self.inverse.setdefault(value,[]).append(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
self.inverse[self[key]].remove(key)
|
||||
self.inverse[self[key]].remove(key)
|
||||
super(bidict, self).__setitem__(key, value)
|
||||
self.inverse.setdefault(value,[]).append(key)
|
||||
self.inverse.setdefault(value,[]).append(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
value = self[key]
|
||||
self.inverse.setdefault(value,[]).remove(key)
|
||||
if value in self.inverse and not self.inverse[value]:
|
||||
if value in self.inverse and not self.inverse[value]:
|
||||
del self.inverse[value]
|
||||
super(bidict, self).__delitem__(key)
|
||||
|
||||
@@ -787,12 +790,11 @@ class bidict(dict):
|
||||
class HexInt(int): pass
|
||||
|
||||
def hex_representer(dumper, data):
|
||||
import yaml
|
||||
return yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#0{4}x}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(make_new_base2current())
|
||||
print("New Rom Hash:", get_new_romhash())
|
||||
# read_entrance_data(old_rom=sys.argv[1])
|
||||
# room_palette_data(old_rom=sys.argv[1])
|
||||
# extract_data_from_us_rom(sys.argv[1])
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GKVersion = '1.1.0'
|
||||
ORVersion = '0.7.0.3'
|
||||
DRVersion = '1.5.6-u'
|
||||
+139
-1
@@ -1 +1,139 @@
|
||||
theme: jekyll-theme-slate
|
||||
# Site settings
|
||||
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 © 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
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
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!*
|
||||
@@ -1,5 +0,0 @@
|
||||
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
|
||||
@@ -1,191 +0,0 @@
|
||||
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.
|
||||
@@ -1,55 +0,0 @@
|
||||
"""collections_extended contains a few extra basic data structures."""
|
||||
from ._compat import Collection
|
||||
from .bags import bag, frozenbag
|
||||
from .setlists import setlist, frozensetlist
|
||||
from .bijection import bijection
|
||||
from .range_map import RangeMap, MappedRange
|
||||
|
||||
__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)
|
||||
@@ -1,53 +0,0 @@
|
||||
"""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 Sized, Iterable, Container
|
||||
|
||||
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
|
||||
@@ -1,16 +0,0 @@
|
||||
"""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
|
||||
@@ -1,527 +0,0 @@
|
||||
"""Bag class definitions."""
|
||||
import heapq
|
||||
from operator import itemgetter
|
||||
from collections import Set, MutableSet, Hashable
|
||||
|
||||
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
|
||||
@@ -1,94 +0,0 @@
|
||||
"""Class definition for bijection."""
|
||||
|
||||
from collections import MutableMapping, Mapping
|
||||
|
||||
|
||||
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
|
||||
@@ -1,384 +0,0 @@
|
||||
"""RangeMap class definition."""
|
||||
from bisect import bisect_left, bisect_right
|
||||
from collections import namedtuple, Mapping, MappingView, Set
|
||||
|
||||
|
||||
# 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.')
|
||||
@@ -1,552 +0,0 @@
|
||||
"""Setlist class definitions."""
|
||||
import random as random_
|
||||
|
||||
from collections import (
|
||||
Sequence,
|
||||
Set,
|
||||
MutableSequence,
|
||||
MutableSet,
|
||||
Hashable,
|
||||
)
|
||||
|
||||
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
|
||||
@@ -1,28 +0,0 @@
|
||||
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
@@ -0,0 +1,100 @@
|
||||
---
|
||||
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!
|
||||
@@ -1,5 +0,0 @@
|
||||
from OverworldShuffle import __version__ as OWVersion
|
||||
import os
|
||||
|
||||
with(open(os.path.join("resources","app","meta","manifests","app_version.txt"),"w+")) as f:
|
||||
f.write(OWVersion)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import sys
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(sys._MEIPASS, "ext"))
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
+66
-3
@@ -68,10 +68,30 @@ 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.
|
||||
|
||||
|
||||
##### Caveat
|
||||
|
||||
**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
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
@@ -234,7 +254,7 @@ You may define a list of items and a list of locations. Those items will be cons
|
||||
|
||||
### 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_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`.
|
||||
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`.
|
||||
|
||||
#### two-way
|
||||
|
||||
@@ -253,6 +273,49 @@ someDescription:
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
@@ -20,7 +20,8 @@ settings:
|
||||
shuffle_followers: true
|
||||
shuffle: crossed
|
||||
shufflelinks: true
|
||||
ow_shuffle: parallel
|
||||
ow_layout: wild
|
||||
ow_parallel: true
|
||||
ow_terrain: true
|
||||
ow_crossed: grouped
|
||||
ow_keepsimilar: true
|
||||
@@ -66,6 +67,14 @@ item_pool:
|
||||
Sanctuary Heart Container: 3
|
||||
Shovel: 3
|
||||
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:
|
||||
1:
|
||||
Palace of Darkness - Big Chest: Hammer
|
||||
|
||||
+473
@@ -0,0 +1,473 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
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
@@ -0,0 +1,369 @@
|
||||
---
|
||||
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
@@ -0,0 +1,366 @@
|
||||
---
|
||||
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!
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
@@ -1,5 +1,6 @@
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
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,6 +1,7 @@
|
||||
settings:
|
||||
1:
|
||||
ow_shuffle: full
|
||||
ow_layout: wild
|
||||
ow_parallel: false
|
||||
ow_keepsimilar: false
|
||||
ow-edges:
|
||||
1:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
settings:
|
||||
1:
|
||||
ow_shuffle: full
|
||||
ow_layout: wild
|
||||
ow_parallel: false
|
||||
ow_keepsimilar: false
|
||||
ow-edges:
|
||||
1:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
settings:
|
||||
1:
|
||||
ow_shuffle: full
|
||||
ow_layout: wild
|
||||
ow_parallel: false
|
||||
ow_keepsimilar: false
|
||||
ow-edges:
|
||||
1:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
groups:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
groups:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
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,5 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow_whirlpool: false
|
||||
ow-edges:
|
||||
1:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
groups:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
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,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_parallel: false
|
||||
ow_whirlpool: false
|
||||
ow-edges:
|
||||
1:
|
||||
@@ -77,6 +78,25 @@ ow-edges:
|
||||
Desert Pass EC*: Dam WC*
|
||||
Desert Pass ES*: Dam WS*
|
||||
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:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
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,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
settings:
|
||||
1:
|
||||
ow_whirlpool: false
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_parallel: false
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
@@ -69,6 +72,25 @@ ow-edges:
|
||||
Swamp Nook EC*: Swamp WC*
|
||||
Swamp Nook ES*: Swamp WS*
|
||||
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:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
groups:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow_terrain: false
|
||||
ow-edges:
|
||||
1:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow_terrain: true
|
||||
ow-edges:
|
||||
1:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
settings:
|
||||
1:
|
||||
ow_layout: wild
|
||||
ow-edges:
|
||||
1:
|
||||
two-way:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user