Merge branch 'master' into pull/58
# Conflicts: # AdjusterMain.py # BaseClasses.py # EntranceShuffle.py # Gui.py # InvertedRegions.py # ItemList.py # Main.py # Plando.py # Rom.py # Rules.py
This commit is contained in:
commit
f89c28d5c2
|
@ -0,0 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: Berserker55 # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@ -1,6 +1,7 @@
|
|||
.idea
|
||||
.vscode
|
||||
*_Spoiler.txt
|
||||
*.bmbp
|
||||
*.pyc
|
||||
*.sfc
|
||||
*.wixobj
|
||||
|
@ -12,4 +13,10 @@ README.html
|
|||
*multidata
|
||||
*multisave
|
||||
EnemizerCLI/
|
||||
.mypy_cache/
|
||||
.mypy_cache/
|
||||
RaceRom.py
|
||||
weights/
|
||||
/MultiMystery/
|
||||
/Players/
|
||||
/QUsb2Snes/
|
||||
/options.yaml
|
||||
|
|
13
Adjuster.py
13
Adjuster.py
|
@ -6,6 +6,7 @@ import textwrap
|
|||
import sys
|
||||
|
||||
from AdjusterMain import adjust
|
||||
from Rom import get_sprite_from_name
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
|
@ -15,7 +16,8 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
|||
def main():
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttPR rom to adjust.')
|
||||
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
|
@ -31,6 +33,8 @@ def main():
|
|||
''')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
|
@ -38,14 +42,15 @@ def main():
|
|||
Alternatively, can be a ALttP Rom patched with a Link
|
||||
sprite that will be extracted.
|
||||
''')
|
||||
parser.add_argument('--names', default='', type=str)
|
||||
args = parser.parse_args()
|
||||
|
||||
# ToDo: Validate files further than mere existance
|
||||
if not os.path.isfile(args.rom):
|
||||
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
input('Could not find valid rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
|
||||
# set up logger
|
||||
|
|
|
@ -3,32 +3,25 @@ import time
|
|||
import logging
|
||||
|
||||
from Utils import output_path
|
||||
from Rom import LocalRom, Sprite, apply_rom_settings
|
||||
from Rom import LocalRom, apply_rom_settings
|
||||
|
||||
|
||||
def adjust(args):
|
||||
start = time.perf_counter()
|
||||
logger = logging.getLogger('')
|
||||
logger = logging.getLogger('Adjuster')
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
if args.sprite is not None:
|
||||
if isinstance(args.sprite, Sprite):
|
||||
sprite = args.sprite
|
||||
else:
|
||||
sprite = Sprite(args.sprite)
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
outfilebase = os.path.basename(args.rom)[:-4] + '_adjusted'
|
||||
|
||||
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
||||
rom = LocalRom(args.rom, False)
|
||||
rom = LocalRom(args.rom, patch=False)
|
||||
if os.path.isfile(args.baserom):
|
||||
baserom = LocalRom(args.baserom, patch=True)
|
||||
rom.orig_buffer = baserom.orig_buffer
|
||||
else:
|
||||
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, sprite)
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||
|
||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||
rom.write_to_file(output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc'))
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.perf_counter() - start)
|
||||
|
|
650
BaseClasses.py
650
BaseClasses.py
File diff suppressed because it is too large
Load Diff
24
Bosses.py
24
Bosses.py
|
@ -76,7 +76,7 @@ def KholdstareDefeatRule(state, player):
|
|||
(
|
||||
state.has('Bombos', player) and
|
||||
# FIXME: the following only actually works for the vanilla location for swordless
|
||||
(state.has_sword(player) or state.world.swords == 'swordless')
|
||||
(state.has_sword(player) or state.world.swords[player] == 'swordless')
|
||||
)
|
||||
) and
|
||||
(
|
||||
|
@ -86,7 +86,7 @@ def KholdstareDefeatRule(state, player):
|
|||
(
|
||||
state.has('Fire Rod', player) and
|
||||
state.has('Bombos', player) and
|
||||
state.world.swords == 'swordless' and
|
||||
state.world.swords[player] == 'swordless' and
|
||||
state.can_extend_magic(player, 16)
|
||||
)
|
||||
)
|
||||
|
@ -120,8 +120,8 @@ boss_table = {
|
|||
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
|
||||
}
|
||||
|
||||
def can_place_boss(world, boss, dungeon_name, level=None):
|
||||
if world.swords in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
|
||||
def can_place_boss(world, player, boss, dungeon_name, level=None):
|
||||
if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
|
||||
return False
|
||||
|
||||
if dungeon_name in ['Ganons Tower', 'Inverted Ganons Tower'] and level == 'top':
|
||||
|
@ -143,10 +143,10 @@ def can_place_boss(world, boss, dungeon_name, level=None):
|
|||
return True
|
||||
|
||||
def place_bosses(world, player):
|
||||
if world.boss_shuffle == 'none':
|
||||
if world.boss_shuffle[player] == 'none':
|
||||
return
|
||||
# Most to least restrictive order
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
boss_locations = [
|
||||
['Ganons Tower', 'top'],
|
||||
['Tower of Hera', None],
|
||||
|
@ -182,15 +182,15 @@ def place_bosses(world, player):
|
|||
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
|
||||
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
|
||||
|
||||
if world.boss_shuffle in ["basic", "normal"]:
|
||||
if world.boss_shuffle[player] in ["basic", "normal"]:
|
||||
# temporary hack for swordless kholdstare:
|
||||
if world.swords == 'swordless':
|
||||
if world.swords[player] == 'swordless':
|
||||
world.get_dungeon('Ice Palace', player).boss = BossFactory('Kholdstare', player)
|
||||
logging.getLogger('').debug('Placing boss Kholdstare at Ice Palace')
|
||||
boss_locations.remove(['Ice Palace', None])
|
||||
placeable_bosses.remove('Kholdstare')
|
||||
|
||||
if world.boss_shuffle == "basic": # vanilla bosses shuffled
|
||||
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
|
||||
|
@ -200,18 +200,18 @@ def place_bosses(world, player):
|
|||
random.shuffle(bosses)
|
||||
for [loc, level] in boss_locations:
|
||||
loc_text = loc + (' ('+level+')' if level else '')
|
||||
boss = next((b for b in bosses if can_place_boss(world, b, loc, level)), None)
|
||||
boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
|
||||
if not boss:
|
||||
raise FillError('Could not place boss for location %s' % loc_text)
|
||||
bosses.remove(boss)
|
||||
|
||||
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
|
||||
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
|
||||
elif world.boss_shuffle == "chaos": #all bosses chosen at random
|
||||
elif world.boss_shuffle[player] == "chaos": #all bosses chosen at random
|
||||
for [loc, level] in boss_locations:
|
||||
loc_text = loc + (' ('+level+')' if level else '')
|
||||
try:
|
||||
boss = random.choice([b for b in placeable_bosses if can_place_boss(world, b, loc, level)])
|
||||
boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)])
|
||||
except IndexError:
|
||||
raise FillError('Could not place boss for location %s' % loc_text)
|
||||
|
||||
|
|
39
Dungeons.py
39
Dungeons.py
|
@ -8,7 +8,7 @@ from Items import ItemFactory
|
|||
|
||||
def create_dungeons(world, player):
|
||||
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro else small_keys, dungeon_items, player)
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys, dungeon_items, player)
|
||||
dungeon.boss = BossFactory(default_boss, player)
|
||||
for region in dungeon.regions:
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
|
@ -27,7 +27,7 @@ def create_dungeons(world, player):
|
|||
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], 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', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
|
||||
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
|
||||
else:
|
||||
|
@ -47,7 +47,7 @@ def fill_dungeons(world):
|
|||
|
||||
for player in range(1, world.players + 1):
|
||||
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
|
||||
else:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
|
||||
|
@ -113,21 +113,20 @@ def fill_dungeons(world):
|
|||
continue
|
||||
|
||||
# next place dungeon items
|
||||
if world.place_dungeon_items:
|
||||
for dungeon_item in dungeon_items:
|
||||
di_location = dungeon_locations.pop()
|
||||
world.push_item(di_location, dungeon_item, False)
|
||||
for dungeon_item in dungeon_items:
|
||||
di_location = dungeon_locations.pop()
|
||||
world.push_item(di_location, dungeon_item, False)
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.key or world.place_dungeon_items]
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
|
||||
def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
|
||||
else:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
|
||||
|
@ -135,22 +134,24 @@ def fill_dungeons_restrictive(world, shuffled_locations):
|
|||
pinball_room.locked = True
|
||||
shuffled_locations.remove(pinball_room)
|
||||
|
||||
if world.keysanity:
|
||||
#in keysanity dungeon items are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if item.key:
|
||||
item.advancement = True
|
||||
elif item.map or item.compass:
|
||||
item.priority = True
|
||||
return
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
all_state_base.collect(item, True)
|
||||
item.advancement = True
|
||||
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
|
||||
item.priority = True
|
||||
|
||||
dungeon_items = get_dungeon_item_pool(world)
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player]))]
|
||||
|
||||
# sort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
|
||||
|
||||
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items)
|
||||
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, True)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,21 @@ Hints will appear in the following ratios across the 15 telepathic tiles that ha
|
|||
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, and restricted legacy 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]".
|
||||
|
@ -65,7 +80,12 @@ Spike Cave
|
|||
Magic Bat
|
||||
Sahasrahla (Green Pendant)
|
||||
|
||||
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 keysanity is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
|
||||
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:
|
||||
|
||||
|
@ -103,8 +123,8 @@ 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 southmost cave on east DM
|
||||
Paradox Cave (Middle): The right paired cave on east 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
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import logging
|
||||
import random
|
||||
import textwrap
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
from Main import main
|
||||
from Utils import is_bundled, close_console, output_path
|
||||
from Rom import get_sprite_from_name
|
||||
from Utils import is_bundled, close_console
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
@ -15,11 +18,18 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
|||
def _get_help_string(self, action):
|
||||
return textwrap.dedent(action.help)
|
||||
|
||||
def parse_arguments(argv, no_defaults=False):
|
||||
def defval(value):
|
||||
return value if not no_defaults else None
|
||||
|
||||
# we need to know how many players we have first
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
multiargs, _ = parser.parse_known_args(argv)
|
||||
|
||||
def start():
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
|
||||
parser.add_argument('--logic', default='noglitches', const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
|
||||
parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
|
||||
help='''\
|
||||
Select Enforcement of Item Requirements. (default: %(default)s)
|
||||
No Glitches:
|
||||
|
@ -28,7 +38,7 @@ def start():
|
|||
No Logic: Distribute items without regard for
|
||||
item requirements.
|
||||
''')
|
||||
parser.add_argument('--mode', default='open', const='open', nargs='?', choices=['standard', 'open', 'inverted'],
|
||||
parser.add_argument('--mode', default=defval('open'), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
|
||||
help='''\
|
||||
Select game mode. (default: %(default)s)
|
||||
Open: World starts with Zelda rescued.
|
||||
|
@ -41,7 +51,7 @@ def start():
|
|||
Requires the moon pearl to be Link in the Light World
|
||||
instead of a bunny.
|
||||
''')
|
||||
parser.add_argument('--swords', default='random', const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
|
||||
parser.add_argument('--swords', default=defval('random'), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
|
||||
help='''\
|
||||
Select sword placement. (default: %(default)s)
|
||||
Random: All swords placed randomly.
|
||||
|
@ -55,7 +65,7 @@ def start():
|
|||
Palace, to allow for an alternative to firerod.
|
||||
Vanilla: Swords are in vanilla locations.
|
||||
''')
|
||||
parser.add_argument('--goal', default='ganon', const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
|
||||
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
|
||||
help='''\
|
||||
Select completion goal. (default: %(default)s)
|
||||
Ganon: Collect all crystals, beat Agahnim 2 then
|
||||
|
@ -67,21 +77,21 @@ def start():
|
|||
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
|
||||
20 of them to beat the game.
|
||||
''')
|
||||
parser.add_argument('--difficulty', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
|
||||
parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
|
||||
help='''\
|
||||
Select game difficulty. Affects available itempool. (default: %(default)s)
|
||||
Normal: Normal difficulty.
|
||||
Hard: A harder setting with less equipment and reduced health.
|
||||
Expert: A harder yet setting with minimum equipment and health.
|
||||
''')
|
||||
parser.add_argument('--item_functionality', default='normal', const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
|
||||
parser.add_argument('--item_functionality', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
|
||||
help='''\
|
||||
Select limits on item functionality to increase difficulty. (default: %(default)s)
|
||||
Normal: Normal functionality.
|
||||
Hard: Reduced functionality.
|
||||
Expert: Greatly reduced functionality.
|
||||
''')
|
||||
parser.add_argument('--timer', default='none', const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
|
||||
parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
|
||||
help='''\
|
||||
Select game timer setting. Affects available itempool. (default: %(default)s)
|
||||
None: No timer.
|
||||
|
@ -101,7 +111,7 @@ def start():
|
|||
Timed mode. If time runs out, you lose (but can
|
||||
still keep playing).
|
||||
''')
|
||||
parser.add_argument('--progressive', default='on', const='normal', nargs='?', choices=['on', 'off', 'random'],
|
||||
parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
|
||||
help='''\
|
||||
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
|
||||
On: Swords, Shields, Armor, and Gloves will
|
||||
|
@ -115,7 +125,7 @@ def start():
|
|||
category, be randomly progressive or not.
|
||||
Link will die in one hit.
|
||||
''')
|
||||
parser.add_argument('--algorithm', default='balanced', const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
|
||||
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
|
||||
help='''\
|
||||
Select item filling algorithm. (default: %(default)s
|
||||
balanced: vt26 derivitive that aims to strike a balance between
|
||||
|
@ -138,7 +148,7 @@ def start():
|
|||
slightly biased to placing progression items with
|
||||
less restrictions.
|
||||
''')
|
||||
parser.add_argument('--shuffle', default='full', const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
parser.add_argument('--shuffle', default=defval('full'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
|
||||
help='''\
|
||||
Select Entrance Shuffling Algorithm. (default: %(default)s)
|
||||
Full: Mix cave and dungeon entrances freely while limiting
|
||||
|
@ -162,7 +172,7 @@ def start():
|
|||
The dungeon variants only mix up dungeons and keep the rest of
|
||||
the overworld vanilla.
|
||||
''')
|
||||
parser.add_argument('--crystals_ganon', default='7', const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
help='''\
|
||||
How many crystals are needed to defeat ganon. Any other
|
||||
requirements for ganon for the selected goal still apply.
|
||||
|
@ -171,16 +181,18 @@ def start():
|
|||
Random: Picks a random value between 0 and 7 (inclusive).
|
||||
0-7: Number of crystals needed
|
||||
''')
|
||||
parser.add_argument('--crystals_gt', default='7', const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
parser.add_argument('--crystals_gt', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
help='''\
|
||||
How many crystals are needed to open GT. For inverted mode
|
||||
this applies to the castle tower door instead. (default: %(default)s)
|
||||
Random: Picks a random value between 0 and 7 (inclusive).
|
||||
0-7: Number of crystals needed
|
||||
''')
|
||||
|
||||
parser.add_argument('--rom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--openpyramid', default=defval(False), help='''\
|
||||
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
|
||||
''', action='store_true')
|
||||
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'), help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--count', help='''\
|
||||
Use to batch generate multiple seeds with same settings.
|
||||
|
@ -189,50 +201,51 @@ def start():
|
|||
--seed given will produce the same 10 (different) roms each
|
||||
time).
|
||||
''', type=int)
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
||||
parser.add_argument('--keysanity', help='''\
|
||||
Keys (and other dungeon items) are no longer restricted to
|
||||
their dungeons, but can be anywhere
|
||||
''', action='store_true')
|
||||
parser.add_argument('--retro', help='''\
|
||||
parser.add_argument('--extendedmsu', help='Use v31 Extended msu', action='store_true')
|
||||
parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
|
||||
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
|
||||
parser.add_argument('--retro', default=defval(False), help='''\
|
||||
Keys are universal, shooting arrows costs rupees,
|
||||
and a few other little things make this more like Zelda-1.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--custom', default=False, help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=False, help='Not supported.')
|
||||
parser.add_argument('--nodungeonitems', help='''\
|
||||
Remove Maps and Compasses from Itempool, replacing them by
|
||||
empty slots.
|
||||
''', action='store_true')
|
||||
parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
|
||||
parser.add_argument('--custom', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
|
||||
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
|
||||
Select Item/Location Accessibility. (default: %(default)s)
|
||||
Items: You can reach all unique inventory items. No guarantees about
|
||||
reaching all locations or all keys.
|
||||
Locations: You will be able to reach every location in the game.
|
||||
None: You will be able to reach enough locations to beat the game.
|
||||
''')
|
||||
parser.add_argument('--hints', help='''\
|
||||
parser.add_argument('--hints', default=defval(False), help='''\
|
||||
Make telepathic tiles and storytellers give helpful hints.
|
||||
''', action='store_true')
|
||||
# included for backwards compatibility
|
||||
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=True)
|
||||
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
|
||||
parser.add_argument('--no-shuffleganon', help='''\
|
||||
If set, the Pyramid Hole and Ganon's Tower are not
|
||||
included entrance shuffle pool.
|
||||
''', action='store_false', dest='shuffleganon')
|
||||
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
|
||||
help='''\
|
||||
Select the rate at which the heart beep sound is played at
|
||||
low health. (default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='''\
|
||||
Path to a sprite sheet to use for Link. Needs to be in
|
||||
binary format and have a length of 0x7000 (28672) bytes,
|
||||
|
@ -246,21 +259,58 @@ def start():
|
|||
Output .json patch to stdout instead of a patched rom. Used
|
||||
for VT site integration, do not use otherwise.
|
||||
''')
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=False)
|
||||
parser.add_argument('--enemizercli', default='')
|
||||
parser.add_argument('--shufflebosses', default='none', choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--shuffleenemies', default=False, action='store_true')
|
||||
parser.add_argument('--enemy_health', default='default', choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default='default', choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepalette', default=False, action='store_true')
|
||||
parser.add_argument('--shufflepots', default=False, action='store_true')
|
||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
|
||||
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
|
||||
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||
parser.add_argument('--remote_items', default=defval(False), action='store_true')
|
||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--names', default=defval(''))
|
||||
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--outputpath')
|
||||
args = parser.parse_args()
|
||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
parser.add_argument('--create_diff', default=defval(False), action='store_true', help='''\
|
||||
create a binary patch file from which the randomized rom can be recreated using MultiClient.
|
||||
Does not work with jsonout.''')
|
||||
|
||||
if args.outputpath and os.path.isdir(args.outputpath):
|
||||
output_path.cached_path = args.outputpath
|
||||
if multiargs.multi:
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
||||
|
||||
ret = parser.parse_args(argv)
|
||||
if ret.timer == "none":
|
||||
ret.timer = False
|
||||
if ret.keysanity:
|
||||
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
|
||||
|
||||
if multiargs.multi:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
playerargs = parse_arguments(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'timer',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'heartbeep',
|
||||
'remote_items', 'progressive', 'extendedmsu']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
else:
|
||||
getattr(ret, name)[player] = value
|
||||
|
||||
return ret
|
||||
|
||||
def start():
|
||||
args = parse_arguments(None)
|
||||
|
||||
if is_bundled() and len(sys.argv) == 1:
|
||||
# for the bundled builds, if we have no arguments, the user
|
||||
|
@ -276,9 +326,9 @@ def start():
|
|||
if not args.jsonout and not os.path.isfile(args.rom):
|
||||
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite):
|
||||
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
|
||||
if not args.jsonout:
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise IOError('Cannot find sprite file at %s' % args.sprite)
|
||||
|
|
|
@ -19,17 +19,17 @@ def link_entrances(world, player):
|
|||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
# if we do not shuffle, set default connections
|
||||
if world.shuffle == 'vanilla':
|
||||
if world.shuffle[player] == 'vanilla':
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
for exitname, regionname in default_dungeon_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
elif world.shuffle == 'dungeonssimple':
|
||||
elif world.shuffle[player] == 'dungeonssimple':
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
simple_shuffle_dungeons(world, player)
|
||||
elif world.shuffle == 'dungeonsfull':
|
||||
elif world.shuffle[player] == 'dungeonsfull':
|
||||
for exitname, regionname in default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
|
@ -39,7 +39,7 @@ def link_entrances(world, player):
|
|||
lw_entrances = list(LW_Dungeon_Entrances)
|
||||
dw_entrances = list(DW_Dungeon_Entrances)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
|
@ -52,14 +52,14 @@ def link_entrances(world, player):
|
|||
dw_entrances.append('Ganons Tower')
|
||||
dungeon_exits.append('Ganons Tower Exit')
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
|
||||
connect_mandatory_exits(world, lw_entrances, [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], list(LW_Dungeon_Entrances_Must_Exit), player)
|
||||
else:
|
||||
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
|
||||
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
|
||||
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
||||
elif world.shuffle == 'simple':
|
||||
elif world.shuffle[player] == 'simple':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
old_man_entrances = list(Old_Man_Entrances)
|
||||
|
@ -130,7 +130,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'restricted':
|
||||
elif world.shuffle[player] == 'restricted':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
||||
|
@ -201,7 +201,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'restricted_legacy':
|
||||
elif world.shuffle[player] == 'restricted_legacy':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
lw_entrances = list(LW_Entrances)
|
||||
|
@ -256,7 +256,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'full':
|
||||
elif world.shuffle[player] == 'full':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
||||
|
@ -273,7 +273,7 @@ def link_entrances(world, player):
|
|||
# tavern back door cannot be shuffled yet
|
||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
|
@ -309,7 +309,7 @@ def link_entrances(world, player):
|
|||
pass
|
||||
else: #if the cave wasn't placed we get here
|
||||
connect_caves(world, lw_entrances, [], old_man_house, player)
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# rest of hyrule castle must be in light world
|
||||
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||
|
||||
|
@ -361,7 +361,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'crossed':
|
||||
elif world.shuffle[player] == 'crossed':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
|
||||
|
@ -376,7 +376,7 @@ def link_entrances(world, player):
|
|||
# tavern back door cannot be shuffled yet
|
||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
|
@ -392,7 +392,7 @@ def link_entrances(world, player):
|
|||
#place must-exit caves
|
||||
connect_mandatory_exits(world, entrances, caves, must_exits, player)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# rest of hyrule castle must be dealt with
|
||||
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||
|
||||
|
@ -437,7 +437,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, entrances, door_targets, player)
|
||||
elif world.shuffle == 'full_legacy':
|
||||
elif world.shuffle[player] == 'full_legacy':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
|
||||
|
@ -451,7 +451,7 @@ def link_entrances(world, player):
|
|||
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
|
||||
door_targets = list(Single_Cave_Targets)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
else:
|
||||
|
@ -471,7 +471,7 @@ def link_entrances(world, player):
|
|||
else:
|
||||
connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
|
||||
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# rest of hyrule castle must be in light world
|
||||
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||
|
||||
|
@ -513,7 +513,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'madness_legacy':
|
||||
elif world.shuffle[player] == 'madness_legacy':
|
||||
# here lie dragons, connections are no longer two way
|
||||
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
|
||||
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
|
||||
|
@ -552,7 +552,7 @@ def link_entrances(world, player):
|
|||
('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
|
||||
(('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Woods Second Section (Drop)')]
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# cannot move uncle cave
|
||||
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
||||
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
|
||||
|
@ -606,7 +606,7 @@ def link_entrances(world, player):
|
|||
connect_entrance(world, hole, target, player)
|
||||
|
||||
# hyrule castle handling
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
|
||||
|
@ -755,7 +755,7 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
elif world.shuffle == 'insanity':
|
||||
elif world.shuffle[player] == 'insanity':
|
||||
# beware ye who enter here
|
||||
|
||||
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
|
||||
|
@ -792,7 +792,7 @@ def link_entrances(world, player):
|
|||
# tavern back door cannot be shuffled yet
|
||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# cannot move uncle cave
|
||||
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
||||
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
|
||||
|
@ -825,7 +825,7 @@ def link_entrances(world, player):
|
|||
connect_entrance(world, hole, hole_targets.pop(), player)
|
||||
|
||||
# hyrule castle handling
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
|
||||
|
@ -902,8 +902,8 @@ def link_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'insanity_legacy':
|
||||
world.fix_fake_world = False
|
||||
elif world.shuffle[player] == 'insanity_legacy':
|
||||
world.fix_fake_world[player] = False
|
||||
# beware ye who enter here
|
||||
|
||||
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
|
||||
|
@ -927,7 +927,7 @@ def link_entrances(world, player):
|
|||
hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Woods Second Section (Drop)',
|
||||
'Skull Woods First Section (Left)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Top)']
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# cannot move uncle cave
|
||||
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
||||
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
|
||||
|
@ -960,7 +960,7 @@ def link_entrances(world, player):
|
|||
connect_entrance(world, hole, hole_targets.pop(), player)
|
||||
|
||||
# hyrule castle handling
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# must connect front of hyrule castle to do escape
|
||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
|
||||
|
@ -1079,17 +1079,17 @@ def link_inverted_entrances(world, player):
|
|||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
# if we do not shuffle, set default connections
|
||||
if world.shuffle == 'vanilla':
|
||||
if world.shuffle[player] == 'vanilla':
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
for exitname, regionname in inverted_default_dungeon_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
elif world.shuffle == 'dungeonssimple':
|
||||
elif world.shuffle[player] == 'dungeonssimple':
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
simple_shuffle_dungeons(world, player)
|
||||
elif world.shuffle == 'dungeonsfull':
|
||||
elif world.shuffle[player] == 'dungeonsfull':
|
||||
for exitname, regionname in inverted_default_connections:
|
||||
connect_simple(world, exitname, regionname, player)
|
||||
|
||||
|
@ -1151,7 +1151,7 @@ def link_inverted_entrances(world, player):
|
|||
remaining_lw_entrances = [i for i in all_dungeon_entrances if i in lw_entrances]
|
||||
connect_caves(world, remaining_lw_entrances, remaining_dw_entrances, dungeon_exits, player)
|
||||
|
||||
elif world.shuffle == 'simple':
|
||||
elif world.shuffle[player] == 'simple':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
old_man_entrances = list(Inverted_Old_Man_Entrances)
|
||||
|
@ -1160,7 +1160,7 @@ def link_inverted_entrances(world, player):
|
|||
|
||||
single_doors = list(Single_Cave_Doors)
|
||||
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors)
|
||||
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors)
|
||||
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
|
||||
door_targets = list(Inverted_Single_Cave_Targets)
|
||||
|
||||
# we shuffle all 2 entrance caves as pairs as a start
|
||||
|
@ -1191,6 +1191,8 @@ def link_inverted_entrances(world, player):
|
|||
bomb_shop_doors.remove(links_house)
|
||||
if links_house in blacksmith_doors:
|
||||
blacksmith_doors.remove(links_house)
|
||||
if links_house in old_man_entrances:
|
||||
old_man_entrances.remove(links_house)
|
||||
|
||||
# place dark sanc
|
||||
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors]
|
||||
|
@ -1243,7 +1245,7 @@ def link_inverted_entrances(world, player):
|
|||
# place remaining doors
|
||||
connect_doors(world, single_doors, door_targets, player)
|
||||
|
||||
elif world.shuffle == 'restricted':
|
||||
elif world.shuffle[player] == 'restricted':
|
||||
simple_shuffle_dungeons(world, player)
|
||||
|
||||
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
|
||||
|
@ -1253,7 +1255,7 @@ def link_inverted_entrances(world, player):
|
|||
caves = list(Cave_Exits + Cave_Three_Exits + Old_Man_House)
|
||||
single_doors = list(Single_Cave_Doors)
|
||||
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
door_targets = list(Inverted_Single_Cave_Targets)
|
||||
|
||||
# place links house
|
||||
|
@ -1326,7 +1328,7 @@ def link_inverted_entrances(world, player):
|
|||
doors = lw_entrances + dw_entrances
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'full':
|
||||
elif world.shuffle[player] == 'full':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
|
||||
|
@ -1335,7 +1337,7 @@ def link_inverted_entrances(world, player):
|
|||
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera'])
|
||||
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues
|
||||
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
door_targets = list(Inverted_Single_Cave_Targets)
|
||||
old_man_house = list(Old_Man_House)
|
||||
|
||||
|
@ -1477,7 +1479,7 @@ def link_inverted_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, doors, door_targets, player)
|
||||
elif world.shuffle == 'crossed':
|
||||
elif world.shuffle[player] == 'crossed':
|
||||
skull_woods_shuffle(world, player)
|
||||
|
||||
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
|
||||
|
@ -1486,7 +1488,7 @@ def link_inverted_entrances(world, player):
|
|||
old_man_entrances = list(Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Inverted Agahnims Tower', 'Tower of Hera'])
|
||||
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits + Old_Man_House) # don't need to consider three exit caves, have one exit caves to avoid parity issues
|
||||
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
door_targets = list(Inverted_Single_Cave_Targets)
|
||||
|
||||
# randomize which desert ledge door is a must-exit
|
||||
|
@ -1587,7 +1589,7 @@ def link_inverted_entrances(world, player):
|
|||
|
||||
# place remaining doors
|
||||
connect_doors(world, entrances, door_targets, player)
|
||||
elif world.shuffle == 'insanity':
|
||||
elif world.shuffle[player] == 'insanity':
|
||||
# beware ye who enter here
|
||||
|
||||
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
|
||||
|
@ -1610,7 +1612,7 @@ def link_inverted_entrances(world, player):
|
|||
# bomb shop logic for.
|
||||
# Specifically we could potentially add: 'Dark Death Mountain Ledge (East)' and doors associated with pits
|
||||
bomb_shop_doors = list(Inverted_Bomb_Shop_Single_Cave_Doors + Inverted_Bomb_Shop_Multi_Cave_Doors + ['Turtle Rock Isolated Ledge Entrance', 'Hookshot Cave Back Entrance'])
|
||||
blacksmith_doors = list(Inverted_Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
blacksmith_doors = list(Blacksmith_Single_Cave_Doors + Blacksmith_Multi_Cave_Doors)
|
||||
door_targets = list(Inverted_Single_Cave_Targets)
|
||||
|
||||
random.shuffle(doors)
|
||||
|
@ -1764,7 +1766,7 @@ def connect_simple(world, exitname, regionname, player):
|
|||
world.get_entrance(exitname, player).connect(world.get_region(regionname, player))
|
||||
|
||||
|
||||
def connect_entrance(world, entrancename, exitname, player):
|
||||
def connect_entrance(world, entrancename: str, exitname: str, player: int):
|
||||
entrance = world.get_entrance(entrancename, player)
|
||||
# check if we got an entrance or a region to connect to
|
||||
try:
|
||||
|
@ -1831,7 +1833,7 @@ def scramble_holes(world, player):
|
|||
else:
|
||||
hole_targets.append(('Pyramid Exit', 'Pyramid'))
|
||||
|
||||
if world.mode == 'standard':
|
||||
if world.mode[player] == 'standard':
|
||||
# cannot move uncle cave
|
||||
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
|
||||
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
||||
|
@ -1840,14 +1842,14 @@ def scramble_holes(world, player):
|
|||
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
||||
|
||||
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
|
||||
if world.shuffle == 'crossed':
|
||||
if world.shuffle[player] == 'crossed':
|
||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||
if world.shuffle_ganon:
|
||||
random.shuffle(hole_targets)
|
||||
exit, target = hole_targets.pop()
|
||||
connect_two_way(world, 'Pyramid Entrance', exit, player)
|
||||
connect_entrance(world, 'Pyramid Hole', target, player)
|
||||
if world.shuffle != 'crossed':
|
||||
if world.shuffle[player] != 'crossed':
|
||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||
|
||||
random.shuffle(hole_targets)
|
||||
|
@ -1882,14 +1884,14 @@ def scramble_inverted_holes(world, player):
|
|||
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
||||
|
||||
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
|
||||
if world.shuffle == 'crossed':
|
||||
if world.shuffle[player] == 'crossed':
|
||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||
if world.shuffle_ganon:
|
||||
random.shuffle(hole_targets)
|
||||
exit, target = hole_targets.pop()
|
||||
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
|
||||
connect_entrance(world, 'Inverted Pyramid Hole', target, player)
|
||||
if world.shuffle != 'crossed':
|
||||
if world.shuffle[player] != 'crossed':
|
||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||
|
||||
random.shuffle(hole_targets)
|
||||
|
@ -1931,11 +1933,11 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, dp_m
|
|||
if len(cave) == 2:
|
||||
entrance = entrances.pop()
|
||||
# ToDo Better solution, this is a hot fix. Do not connect both sides of trock/desert ledge only to each other
|
||||
if world.mode != 'inverted' and entrance == 'Dark Death Mountain Ledge (West)':
|
||||
if world.mode[player] != 'inverted' and entrance == 'Dark Death Mountain Ledge (West)':
|
||||
new_entrance = entrances.pop()
|
||||
entrances.append(entrance)
|
||||
entrance = new_entrance
|
||||
if world.mode == 'inverted' and entrance == dp_must_exit:
|
||||
if world.mode[player] == 'inverted' and entrance == dp_must_exit:
|
||||
new_entrance = entrances.pop()
|
||||
entrances.append(entrance)
|
||||
entrance = new_entrance
|
||||
|
@ -2006,7 +2008,7 @@ def simple_shuffle_dungeons(world, player):
|
|||
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
|
||||
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
if not world.shuffle_ganon:
|
||||
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
|
||||
else:
|
||||
|
@ -2021,13 +2023,13 @@ def simple_shuffle_dungeons(world, player):
|
|||
|
||||
# mix up 4 door dungeons
|
||||
multi_dungeons = ['Desert', 'Turtle Rock']
|
||||
if world.mode == 'open' or (world.mode == 'inverted' and world.shuffle_ganon):
|
||||
if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon):
|
||||
multi_dungeons.append('Hyrule Castle')
|
||||
random.shuffle(multi_dungeons)
|
||||
|
||||
dp_target = multi_dungeons[0]
|
||||
tr_target = multi_dungeons[1]
|
||||
if world.mode not in ['open', 'inverted'] or (world.mode == 'inverted' and world.shuffle_ganon is False):
|
||||
if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False):
|
||||
# place hyrule castle as intended
|
||||
hc_target = 'Hyrule Castle'
|
||||
else:
|
||||
|
@ -2035,7 +2037,7 @@ def simple_shuffle_dungeons(world, player):
|
|||
|
||||
# ToDo improve this?
|
||||
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
if hc_target == 'Hyrule Castle':
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
|
||||
|
@ -2646,8 +2648,6 @@ Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
|
|||
'Desert Palace Entrance (West)',
|
||||
'Desert Palace Entrance (North)']
|
||||
|
||||
Inverted_Blacksmith_Multi_Cave_Doors = [] # same as non-inverted
|
||||
|
||||
Inverted_LW_Single_Cave_Doors = LW_Single_Cave_Doors + ['Inverted Big Bomb Shop']
|
||||
|
||||
Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)',
|
||||
|
@ -2715,39 +2715,8 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing',
|
|||
'Bumper Cave (Top)',
|
||||
'Mimic Cave',
|
||||
'Dark Lake Hylia Shop',
|
||||
'Inverted Links House']
|
||||
|
||||
Inverted_Blacksmith_Single_Cave_Doors = ['Blinds Hideout',
|
||||
'Lake Hylia Fairy',
|
||||
'Light Hype Fairy',
|
||||
'Desert Fairy',
|
||||
'Chicken House',
|
||||
'Aginahs Cave',
|
||||
'Sahasrahlas Hut',
|
||||
'Cave Shop (Lake Hylia)',
|
||||
'Blacksmiths Hut',
|
||||
'Sick Kids House',
|
||||
'Lost Woods Gamble',
|
||||
'Fortune Teller (Light)',
|
||||
'Snitch Lady (East)',
|
||||
'Snitch Lady (West)',
|
||||
'Bush Covered House',
|
||||
'Tavern (Front)',
|
||||
'Light World Bomb Hut',
|
||||
'Kakariko Shop',
|
||||
'Mini Moldorm Cave',
|
||||
'Long Fairy Cave',
|
||||
'Good Bee Cave',
|
||||
'20 Rupee Cave',
|
||||
'50 Rupee Cave',
|
||||
'Ice Rod Cave',
|
||||
'Library',
|
||||
'Potion Shop',
|
||||
'Dam',
|
||||
'Lumberjack House',
|
||||
'Lake Hylia Fortune Teller',
|
||||
'Kakariko Gamble Game',
|
||||
'Inverted Big Bomb Shop']
|
||||
'Inverted Links House',
|
||||
'Inverted Big Bomb Shop']
|
||||
|
||||
|
||||
Inverted_Single_Cave_Targets = ['Blinds Hideout',
|
||||
|
@ -3020,7 +2989,7 @@ inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia
|
|||
('Lake Hylia Island Pier', 'Lake Hylia Island'),
|
||||
('Lake Hylia Warp', 'Northeast Light World'),
|
||||
('Northeast Light World Warp', 'Light World'),
|
||||
('Zoras River', 'Zoras River'),
|
||||
('Zoras River', 'Zoras River'),
|
||||
('Kings Grave Outer Rocks', 'Kings Grave Area'),
|
||||
('Kings Grave Inner Rocks', 'Light World'),
|
||||
('Kakariko Well (top to bottom)', 'Kakariko Well (bottom)'),
|
||||
|
|
137
Fill.py
137
Fill.py
|
@ -161,7 +161,7 @@ def distribute_items_staleness(world):
|
|||
logging.getLogger('').debug('Unplaced items: %s - Unfilled Locations: %s', [item.name for item in itempool], [location.name for location in fill_locations])
|
||||
|
||||
|
||||
def fill_restrictive(world, base_state, locations, itempool):
|
||||
def fill_restrictive(world, base_state, locations, itempool, single_player_placement = False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
|
@ -169,45 +169,51 @@ def fill_restrictive(world, base_state, locations, itempool):
|
|||
new_state.sweep_for_events()
|
||||
return new_state
|
||||
|
||||
while itempool and locations:
|
||||
items_to_place = []
|
||||
nextpool = []
|
||||
placing_players = set()
|
||||
for item in reversed(itempool):
|
||||
if item.player not in placing_players:
|
||||
placing_players.add(item.player)
|
||||
items_to_place.append(item)
|
||||
else:
|
||||
nextpool.insert(0, item)
|
||||
itempool = nextpool
|
||||
unplaced_items = []
|
||||
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
no_access_checks = {}
|
||||
reachable_items = {}
|
||||
for item in itempool:
|
||||
if world.accessibility[item.player] == 'none':
|
||||
no_access_checks.setdefault(item.player, []).append(item)
|
||||
else:
|
||||
reachable_items.setdefault(item.player, []).append(item)
|
||||
|
||||
perform_access_check = True
|
||||
if world.accessibility == 'none':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state)
|
||||
for player_items in [no_access_checks, reachable_items]:
|
||||
while any(player_items.values()) and locations:
|
||||
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
spot_to_fill = None
|
||||
for location in locations:
|
||||
if location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||
spot_to_fill = location
|
||||
break
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
if spot_to_fill is None:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
if world.can_beat_game():
|
||||
if world.accessibility != 'none':
|
||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
|
||||
continue
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
for item_to_place in items_to_place:
|
||||
perform_access_check = True
|
||||
if world.accessibility[item_to_place.player] == 'none':
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
locations.remove(spot_to_fill)
|
||||
spot_to_fill.event = True
|
||||
spot_to_fill = None
|
||||
for location in locations:
|
||||
if (not single_player_placement or location.player == item_to_place.player)\
|
||||
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
|
||||
spot_to_fill = location
|
||||
break
|
||||
|
||||
if spot_to_fill is None:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.insert(0, item_to_place)
|
||||
if world.can_beat_game():
|
||||
if world.accessibility[item_to_place.player] != 'none':
|
||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
|
||||
continue
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
|
||||
def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=None):
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
locations.remove(spot_to_fill)
|
||||
spot_to_fill.event = True
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
||||
# If not passed in, then get a shuffled list of locations to fill in
|
||||
if not fill_locations:
|
||||
fill_locations = world.get_unfilled_locations()
|
||||
|
@ -221,23 +227,26 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No
|
|||
|
||||
# fill in gtower locations with trash first
|
||||
for player in range(1, world.players + 1):
|
||||
if world.ganonstower_vanilla[player]:
|
||||
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
|
||||
random.shuffle(gtower_locations)
|
||||
trashcnt = 0
|
||||
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = restitempool.pop()
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
trashcnt += 1
|
||||
if not gftower_trash or not world.ganonstower_vanilla[player]:
|
||||
continue
|
||||
|
||||
gftower_trash_count = (random.randint(15, 50) if world.goal[player] == 'triforcehunt' else random.randint(0, 15))
|
||||
|
||||
gtower_locations = [location for location in fill_locations if 'Ganons Tower' in location.name and location.player == player]
|
||||
random.shuffle(gtower_locations)
|
||||
trashcnt = 0
|
||||
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = restitempool.pop()
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
trashcnt += 1
|
||||
|
||||
random.shuffle(fill_locations)
|
||||
fill_locations.reverse()
|
||||
|
||||
# Make sure the escape small key is placed first in standard keysanity to prevent running out of spots
|
||||
if world.keysanity and world.mode == 'standard':
|
||||
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0)
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.mode[item.player] == 'standard' and world.keyshuffle[item.player] else 0)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
|
@ -307,7 +316,7 @@ def flood_items(world):
|
|||
location_list = world.get_reachable_locations()
|
||||
random.shuffle(location_list)
|
||||
for location in location_list:
|
||||
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key:
|
||||
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey:
|
||||
# safe to replace
|
||||
replace_item = location.item
|
||||
replace_item.location = None
|
||||
|
@ -327,8 +336,7 @@ def balance_multiworld_progression(world):
|
|||
reachable_locations_count[player] = 0
|
||||
|
||||
def get_sphere_locations(sphere_state, locations):
|
||||
if not world.keysanity:
|
||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||
return [loc for loc in locations if sphere_state.can_reach(loc)]
|
||||
|
||||
while True:
|
||||
|
@ -338,8 +346,7 @@ def balance_multiworld_progression(world):
|
|||
reachable_locations_count[location.player] += 1
|
||||
|
||||
if checked_locations:
|
||||
average_reachable_locations = sum(reachable_locations_count.values()) / world.players
|
||||
threshold = ((average_reachable_locations + max(reachable_locations_count.values())) / 2) * 0.8 #todo: probably needs some tweaking
|
||||
threshold = max(reachable_locations_count.values()) - 20
|
||||
|
||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
|
||||
if balancing_players:
|
||||
|
@ -350,9 +357,9 @@ def balance_multiworld_progression(world):
|
|||
candidate_items = []
|
||||
while True:
|
||||
for location in balancing_sphere:
|
||||
if location.event:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
balancing_state.collect(location.item, True, location)
|
||||
if location.item.player in balancing_players:
|
||||
if location.item.player in balancing_players and not location.locked:
|
||||
candidate_items.append(location)
|
||||
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
||||
for location in balancing_sphere:
|
||||
|
@ -360,11 +367,14 @@ def balance_multiworld_progression(world):
|
|||
balancing_reachables[location.player] += 1
|
||||
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
|
||||
break
|
||||
elif not balancing_sphere:
|
||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
||||
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
|
||||
items_to_replace = []
|
||||
for player in balancing_players:
|
||||
locations_to_test = [l for l in unlocked_locations if l.player == player]
|
||||
# only replace items that end up in another player's world
|
||||
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
|
||||
while items_to_test:
|
||||
testing = items_to_test.pop()
|
||||
|
@ -374,9 +384,6 @@ def balance_multiworld_progression(world):
|
|||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
|
||||
if testing.locked:
|
||||
continue
|
||||
|
||||
if world.has_beaten_game(balancing_state):
|
||||
if not world.has_beaten_game(reducing_state):
|
||||
items_to_replace.append(testing)
|
||||
|
@ -386,13 +393,17 @@ def balance_multiworld_progression(world):
|
|||
items_to_replace.append(testing)
|
||||
|
||||
replaced_items = False
|
||||
locations_for_replacing = [l for l in checked_locations if not l.event and not l.locked]
|
||||
while locations_for_replacing and items_to_replace:
|
||||
new_location = locations_for_replacing.pop()
|
||||
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
|
||||
while replacement_locations and items_to_replace:
|
||||
new_location = replacement_locations.pop()
|
||||
old_location = items_to_replace.pop()
|
||||
|
||||
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
|
||||
replacement_locations.insert(0, new_location)
|
||||
new_location = replacement_locations.pop()
|
||||
|
||||
new_location.item, old_location.item = old_location.item, new_location.item
|
||||
new_location.event = True
|
||||
old_location.event = False
|
||||
new_location.event, old_location.event = True, False
|
||||
state.collect(new_location.item, True, new_location)
|
||||
replaced_items = True
|
||||
if replaced_items:
|
||||
|
@ -402,9 +413,11 @@ def balance_multiworld_progression(world):
|
|||
sphere_locations.append(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
if location.event:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations.extend(sphere_locations)
|
||||
|
||||
if world.has_beaten_game(state):
|
||||
break
|
||||
elif not sphere_locations:
|
||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||
|
|
333
Gui.py
333
Gui.py
|
@ -2,6 +2,7 @@
|
|||
from argparse import Namespace
|
||||
from glob import glob
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import os
|
||||
import shutil
|
||||
|
@ -9,7 +10,11 @@ from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, T
|
|||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from AdjusterMain import adjust
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
||||
from Main import main, __version__ as ESVersion
|
||||
from Rom import Sprite
|
||||
|
@ -58,16 +63,20 @@ def guiMain(args=None):
|
|||
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
|
||||
suppressRomVar = IntVar()
|
||||
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
|
||||
quickSwapVar = IntVar()
|
||||
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
|
||||
keysanityVar = IntVar()
|
||||
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar)
|
||||
openpyramidVar = IntVar()
|
||||
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
|
||||
mcsbshuffleFrame = Frame(checkBoxFrame)
|
||||
mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
|
||||
mapshuffleVar = IntVar()
|
||||
mapshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Maps", variable=mapshuffleVar)
|
||||
compassshuffleVar = IntVar()
|
||||
compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar)
|
||||
keyshuffleVar = IntVar()
|
||||
keyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Keys", variable=keyshuffleVar)
|
||||
bigkeyshuffleVar = IntVar()
|
||||
bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar)
|
||||
retroVar = IntVar()
|
||||
retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar)
|
||||
dungeonItemsVar = IntVar()
|
||||
dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar)
|
||||
disableMusicVar = IntVar()
|
||||
disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar)
|
||||
shuffleGanonVar = IntVar()
|
||||
shuffleGanonVar.set(1) #set default
|
||||
shuffleGanonCheckbutton = Checkbutton(checkBoxFrame, text="Include Ganon's Tower and Pyramid Hole in shuffle pool", variable=shuffleGanonVar)
|
||||
|
@ -79,61 +88,31 @@ def guiMain(args=None):
|
|||
|
||||
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
||||
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
||||
quickSwapCheckbutton.pack(expand=True, anchor=W)
|
||||
keysanityCheckbutton.pack(expand=True, anchor=W)
|
||||
openpyramidCheckbutton.pack(expand=True, anchor=W)
|
||||
mcsbshuffleFrame.pack(expand=True, anchor=W)
|
||||
mcsbLabel.grid(row=0, column=0)
|
||||
mapshuffleCheckbutton.grid(row=0, column=1)
|
||||
compassshuffleCheckbutton.grid(row=0, column=2)
|
||||
keyshuffleCheckbutton.grid(row=0, column=3)
|
||||
bigkeyshuffleCheckbutton.grid(row=0, column=4)
|
||||
retroCheckbutton.pack(expand=True, anchor=W)
|
||||
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
|
||||
disableMusicCheckbutton.pack(expand=True, anchor=W)
|
||||
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
||||
hintsCheckbutton.pack(expand=True, anchor=W)
|
||||
customCheckbutton.pack(expand=True, anchor=W)
|
||||
|
||||
fileDialogFrame = Frame(rightHalfFrame)
|
||||
romOptionsFrame = LabelFrame(rightHalfFrame, text="Rom options")
|
||||
romOptionsFrame.columnconfigure(0, weight=1)
|
||||
romOptionsFrame.columnconfigure(1, weight=1)
|
||||
for i in range(5):
|
||||
romOptionsFrame.rowconfigure(i, weight=1)
|
||||
|
||||
heartbeepFrame = Frame(fileDialogFrame)
|
||||
heartbeepVar = StringVar()
|
||||
heartbeepVar.set('normal')
|
||||
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
|
||||
heartbeepOptionMenu.pack(side=RIGHT)
|
||||
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep sound rate')
|
||||
heartbeepLabel.pack(side=LEFT, padx=(0,52))
|
||||
disableMusicVar = IntVar()
|
||||
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=disableMusicVar)
|
||||
disableMusicCheckbutton.grid(row=0, column=0, sticky=E)
|
||||
|
||||
heartcolorFrame = Frame(fileDialogFrame)
|
||||
heartcolorVar = StringVar()
|
||||
heartcolorVar.set('red')
|
||||
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
|
||||
heartcolorOptionMenu.pack(side=RIGHT)
|
||||
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
|
||||
heartcolorLabel.pack(side=LEFT, padx=(0,127))
|
||||
|
||||
fastMenuFrame = Frame(fileDialogFrame)
|
||||
fastMenuVar = StringVar()
|
||||
fastMenuVar.set('normal')
|
||||
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
fastMenuOptionMenu.pack(side=RIGHT)
|
||||
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
|
||||
fastMenuLabel.pack(side=LEFT, padx=(0,100))
|
||||
|
||||
heartbeepFrame.pack(expand=True, anchor=E)
|
||||
heartcolorFrame.pack(expand=True, anchor=E)
|
||||
fastMenuFrame.pack(expand=True, anchor=E)
|
||||
|
||||
romDialogFrame = Frame(fileDialogFrame)
|
||||
baseRomLabel = Label(romDialogFrame, text='Base Rom')
|
||||
romVar = StringVar()
|
||||
romEntry = Entry(romDialogFrame, textvariable=romVar)
|
||||
|
||||
def RomSelect():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
|
||||
romVar.set(rom)
|
||||
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
romEntry.pack(side=LEFT)
|
||||
romSelectButton.pack(side=LEFT)
|
||||
|
||||
spriteDialogFrame = Frame(fileDialogFrame)
|
||||
baseSpriteLabel = Label(spriteDialogFrame, text='Link Sprite:')
|
||||
spriteDialogFrame = Frame(romOptionsFrame)
|
||||
spriteDialogFrame.grid(row=0, column=1)
|
||||
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
|
||||
|
||||
spriteNameVar = StringVar()
|
||||
sprite = None
|
||||
|
@ -153,17 +132,88 @@ def guiMain(args=None):
|
|||
def SpriteSelect():
|
||||
SpriteSelector(mainWindow, set_sprite)
|
||||
|
||||
spriteSelectButton = Button(spriteDialogFrame, text='Open Sprite Picker', command=SpriteSelect)
|
||||
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
|
||||
|
||||
baseSpriteLabel.pack(side=LEFT)
|
||||
spriteEntry.pack(side=LEFT)
|
||||
spriteSelectButton.pack(side=LEFT)
|
||||
|
||||
romDialogFrame.pack()
|
||||
spriteDialogFrame.pack()
|
||||
quickSwapVar = IntVar()
|
||||
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=quickSwapVar)
|
||||
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
|
||||
|
||||
checkBoxFrame.pack()
|
||||
fileDialogFrame.pack()
|
||||
fastMenuFrame = Frame(romOptionsFrame)
|
||||
fastMenuFrame.grid(row=1, column=1, sticky=E)
|
||||
fastMenuLabel = Label(fastMenuFrame, text='Menu speed')
|
||||
fastMenuLabel.pack(side=LEFT)
|
||||
fastMenuVar = StringVar()
|
||||
fastMenuVar.set('normal')
|
||||
fastMenuOptionMenu = OptionMenu(fastMenuFrame, fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
|
||||
fastMenuOptionMenu.pack(side=LEFT)
|
||||
|
||||
heartcolorFrame = Frame(romOptionsFrame)
|
||||
heartcolorFrame.grid(row=2, column=0, sticky=E)
|
||||
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
|
||||
heartcolorLabel.pack(side=LEFT)
|
||||
heartcolorVar = StringVar()
|
||||
heartcolorVar.set('red')
|
||||
heartcolorOptionMenu = OptionMenu(heartcolorFrame, heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
|
||||
heartcolorOptionMenu.pack(side=LEFT)
|
||||
|
||||
heartbeepFrame = Frame(romOptionsFrame)
|
||||
heartbeepFrame.grid(row=2, column=1, sticky=E)
|
||||
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
|
||||
heartbeepLabel.pack(side=LEFT)
|
||||
heartbeepVar = StringVar()
|
||||
heartbeepVar.set('normal')
|
||||
heartbeepOptionMenu = OptionMenu(heartbeepFrame, heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
|
||||
heartbeepOptionMenu.pack(side=LEFT)
|
||||
|
||||
owPalettesFrame = Frame(romOptionsFrame)
|
||||
owPalettesFrame.grid(row=3, column=0, sticky=E)
|
||||
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
|
||||
owPalettesLabel.pack(side=LEFT)
|
||||
owPalettesVar = StringVar()
|
||||
owPalettesVar.set('default')
|
||||
owPalettesOptionMenu = OptionMenu(owPalettesFrame, owPalettesVar, 'default', 'random', 'blackout')
|
||||
owPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
uwPalettesFrame = Frame(romOptionsFrame)
|
||||
uwPalettesFrame.grid(row=3, column=1, sticky=E)
|
||||
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
|
||||
uwPalettesLabel.pack(side=LEFT)
|
||||
uwPalettesVar = StringVar()
|
||||
uwPalettesVar.set('default')
|
||||
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, uwPalettesVar, 'default', 'random', 'blackout')
|
||||
uwPalettesOptionMenu.pack(side=LEFT)
|
||||
|
||||
romDialogFrame = Frame(romOptionsFrame)
|
||||
romDialogFrame.grid(row=4, column=0, columnspan=2, sticky=W+E)
|
||||
|
||||
baseRomLabel = Label(romDialogFrame, text='Base Rom: ')
|
||||
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
|
||||
romEntry = Entry(romDialogFrame, textvariable=romVar)
|
||||
|
||||
def RomSelect():
|
||||
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
|
||||
import Patch
|
||||
try:
|
||||
Patch.get_base_rom_bytes(rom) # throws error on checksum fail
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while reading ROM", message=str(e))
|
||||
else:
|
||||
romVar.set(rom)
|
||||
romSelectButton['state'] = "disabled"
|
||||
romSelectButton["text"] = "ROM verified"
|
||||
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
romEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
romSelectButton.pack(side=LEFT)
|
||||
|
||||
checkBoxFrame.pack(side=TOP, anchor=W, padx=5, pady=10)
|
||||
romOptionsFrame.pack(expand=True, fill=BOTH, padx=3)
|
||||
|
||||
drowDownFrame = Frame(topFrame)
|
||||
|
||||
|
@ -285,18 +335,19 @@ def guiMain(args=None):
|
|||
algorithmFrame.pack(expand=True, anchor=E)
|
||||
shuffleFrame.pack(expand=True, anchor=E)
|
||||
|
||||
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=5)
|
||||
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2)
|
||||
enemizerFrame.columnconfigure(0, weight=1)
|
||||
enemizerFrame.columnconfigure(1, weight=1)
|
||||
enemizerFrame.columnconfigure(2, weight=1)
|
||||
enemizerFrame.columnconfigure(3, weight=1)
|
||||
|
||||
enemizerPathFrame = Frame(enemizerFrame)
|
||||
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W)
|
||||
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3)
|
||||
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
|
||||
enemizerCLIlabel.pack(side=LEFT)
|
||||
enemizerCLIpathVar = StringVar()
|
||||
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar, width=80)
|
||||
enemizerCLIpathEntry.pack(side=LEFT)
|
||||
enemizerCLIpathVar = StringVar(value="EnemizerCLI/EnemizerCLI.Core")
|
||||
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=enemizerCLIpathVar)
|
||||
enemizerCLIpathEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
def EnemizerSelectPath():
|
||||
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")])
|
||||
if path:
|
||||
|
@ -304,18 +355,21 @@ def guiMain(args=None):
|
|||
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
|
||||
enemizerCLIbrowseButton.pack(side=LEFT)
|
||||
|
||||
enemyShuffleVar = IntVar()
|
||||
enemyShuffleButton = Checkbutton(enemizerFrame, text="Enemy shuffle", variable=enemyShuffleVar)
|
||||
enemyShuffleButton.grid(row=1, column=0)
|
||||
paletteShuffleVar = IntVar()
|
||||
paletteShuffleButton = Checkbutton(enemizerFrame, text="Palette shuffle", variable=paletteShuffleVar)
|
||||
paletteShuffleButton.grid(row=1, column=1)
|
||||
potShuffleVar = IntVar()
|
||||
potShuffleButton = Checkbutton(enemizerFrame, text="Pot shuffle", variable=potShuffleVar)
|
||||
potShuffleButton.grid(row=1, column=2)
|
||||
potShuffleButton.grid(row=0, column=3)
|
||||
|
||||
enemizerEnemyFrame = Frame(enemizerFrame)
|
||||
enemizerEnemyFrame.grid(row=1, column=0, pady=5)
|
||||
enemizerEnemyLabel = Label(enemizerEnemyFrame, text='Enemy shuffle')
|
||||
enemizerEnemyLabel.pack(side=LEFT)
|
||||
enemyShuffleVar = StringVar()
|
||||
enemyShuffleVar.set('none')
|
||||
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos')
|
||||
enemizerEnemyOption.pack(side=LEFT)
|
||||
|
||||
enemizerBossFrame = Frame(enemizerFrame)
|
||||
enemizerBossFrame.grid(row=2, column=0)
|
||||
enemizerBossFrame.grid(row=1, column=1)
|
||||
enemizerBossLabel = Label(enemizerBossFrame, text='Boss shuffle')
|
||||
enemizerBossLabel.pack(side=LEFT)
|
||||
enemizerBossVar = StringVar()
|
||||
|
@ -324,7 +378,7 @@ def guiMain(args=None):
|
|||
enemizerBossOption.pack(side=LEFT)
|
||||
|
||||
enemizerDamageFrame = Frame(enemizerFrame)
|
||||
enemizerDamageFrame.grid(row=2, column=1)
|
||||
enemizerDamageFrame.grid(row=1, column=2)
|
||||
enemizerDamageLabel = Label(enemizerDamageFrame, text='Enemy damage')
|
||||
enemizerDamageLabel.pack(side=LEFT)
|
||||
enemizerDamageVar = StringVar()
|
||||
|
@ -333,7 +387,7 @@ def guiMain(args=None):
|
|||
enemizerDamageOption.pack(side=LEFT)
|
||||
|
||||
enemizerHealthFrame = Frame(enemizerFrame)
|
||||
enemizerHealthFrame.grid(row=2, column=2)
|
||||
enemizerHealthFrame.grid(row=1, column=3)
|
||||
enemizerHealthLabel = Label(enemizerHealthFrame, text='Enemy health')
|
||||
enemizerHealthLabel.pack(side=LEFT)
|
||||
enemizerHealthVar = StringVar()
|
||||
|
@ -346,6 +400,9 @@ def guiMain(args=None):
|
|||
worldLabel = Label(bottomFrame, text='Worlds')
|
||||
worldVar = StringVar()
|
||||
worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar)
|
||||
namesLabel = Label(bottomFrame, text='Player names')
|
||||
namesVar = StringVar()
|
||||
namesEntry = Entry(bottomFrame, textvariable=namesVar)
|
||||
seedLabel = Label(bottomFrame, text='Seed #')
|
||||
seedVar = StringVar()
|
||||
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
|
||||
|
@ -354,8 +411,9 @@ def guiMain(args=None):
|
|||
countSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=countVar)
|
||||
|
||||
def generateRom():
|
||||
guiargs = Namespace
|
||||
guiargs = Namespace()
|
||||
guiargs.multi = int(worldVar.get())
|
||||
guiargs.names = namesVar.get()
|
||||
guiargs.seed = int(seedVar.get()) if seedVar.get() else None
|
||||
guiargs.count = int(countVar.get()) if countVar.get() != '1' else None
|
||||
guiargs.mode = modeVar.get()
|
||||
|
@ -367,6 +425,8 @@ def guiMain(args=None):
|
|||
guiargs.difficulty = difficultyVar.get()
|
||||
guiargs.item_functionality = itemfunctionVar.get()
|
||||
guiargs.timer = timerVar.get()
|
||||
if guiargs.timer == "none":
|
||||
guiargs.timer = False
|
||||
guiargs.progressive = progressiveVar.get()
|
||||
guiargs.accessibility = accessibilityVar.get()
|
||||
guiargs.algorithm = algorithmVar.get()
|
||||
|
@ -376,19 +436,23 @@ def guiMain(args=None):
|
|||
guiargs.fastmenu = fastMenuVar.get()
|
||||
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
||||
guiargs.suppress_rom = bool(suppressRomVar.get())
|
||||
guiargs.keysanity = bool(keysanityVar.get())
|
||||
guiargs.openpyramid = bool(openpyramidVar.get())
|
||||
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
||||
guiargs.compassshuffle = bool(compassshuffleVar.get())
|
||||
guiargs.keyshuffle = bool(keyshuffleVar.get())
|
||||
guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get())
|
||||
guiargs.retro = bool(retroVar.get())
|
||||
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
|
||||
guiargs.quickswap = bool(quickSwapVar.get())
|
||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||
guiargs.ow_palettes = owPalettesVar.get()
|
||||
guiargs.uw_palettes = uwPalettesVar.get()
|
||||
guiargs.shuffleganon = bool(shuffleGanonVar.get())
|
||||
guiargs.hints = bool(hintsVar.get())
|
||||
guiargs.enemizercli = enemizerCLIpathVar.get()
|
||||
guiargs.shufflebosses = enemizerBossVar.get()
|
||||
guiargs.shuffleenemies = bool(enemyShuffleVar.get())
|
||||
guiargs.shuffleenemies = enemyShuffleVar.get()
|
||||
guiargs.enemy_health = enemizerHealthVar.get()
|
||||
guiargs.enemy_damage = enemizerDamageVar.get()
|
||||
guiargs.shufflepalette = bool(paletteShuffleVar.get())
|
||||
guiargs.shufflepots = bool(potShuffleVar.get())
|
||||
guiargs.custom = bool(customVar.get())
|
||||
guiargs.customitemarray = [int(bowVar.get()), int(silverarrowVar.get()), int(boomerangVar.get()), int(magicboomerangVar.get()), int(hookshotVar.get()), int(mushroomVar.get()), int(magicpowderVar.get()), int(firerodVar.get()),
|
||||
|
@ -398,14 +462,19 @@ def guiMain(args=None):
|
|||
int(sword3Var.get()), int(sword4Var.get()), int(progswordVar.get()), int(shield1Var.get()), int(shield2Var.get()), int(shield3Var.get()), int(progshieldVar.get()), int(bluemailVar.get()),
|
||||
int(redmailVar.get()), int(progmailVar.get()), int(halfmagicVar.get()), int(quartermagicVar.get()), int(bcap5Var.get()), int(bcap10Var.get()), int(acap5Var.get()), int(acap10Var.get()),
|
||||
int(arrow1Var.get()), int(arrow10Var.get()), int(bomb1Var.get()), int(bomb3Var.get()), int(rupee1Var.get()), int(rupee5Var.get()), int(rupee20Var.get()), int(rupee50Var.get()), int(rupee100Var.get()),
|
||||
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(triforcepieceVar.get()), int(triforcecountVar.get()),
|
||||
int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
|
||||
int(rupee300Var.get()), int(rupoorVar.get()), int(blueclockVar.get()), int(greenclockVar.get()), int(redclockVar.get()), int(progbowVar.get()), int(bomb10Var.get()), int(triforcepieceVar.get()),
|
||||
int(triforcecountVar.get()), int(triforceVar.get()), int(rupoorcostVar.get()), int(universalkeyVar.get())]
|
||||
guiargs.rom = romVar.get()
|
||||
guiargs.jsonout = None
|
||||
guiargs.sprite = sprite
|
||||
guiargs.skip_playthrough = False
|
||||
guiargs.outputpath = None
|
||||
# get default values for missing parameters
|
||||
for k,v in vars(parse_arguments(['--multi', str(guiargs.multi)])).items():
|
||||
if k not in vars(guiargs):
|
||||
setattr(guiargs, k, v)
|
||||
elif type(v) is dict: # use same settings for every player
|
||||
setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)})
|
||||
try:
|
||||
if not guiargs.suppress_rom and not os.path.exists(guiargs.rom):
|
||||
raise FileNotFoundError(f"Could not find specified rom file {guiargs.rom}")
|
||||
if guiargs.count is not None:
|
||||
seed = guiargs.seed
|
||||
for _ in range(guiargs.count):
|
||||
|
@ -414,6 +483,7 @@ def guiMain(args=None):
|
|||
else:
|
||||
main(seed=guiargs.seed, args=guiargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||
else:
|
||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||
|
@ -422,6 +492,8 @@ def guiMain(args=None):
|
|||
|
||||
worldLabel.pack(side=LEFT)
|
||||
worldSpinbox.pack(side=LEFT)
|
||||
namesLabel.pack(side=LEFT)
|
||||
namesEntry.pack(side=LEFT)
|
||||
seedLabel.pack(side=LEFT, padx=(5, 0))
|
||||
seedEntry.pack(side=LEFT)
|
||||
countLabel.pack(side=LEFT, padx=(5, 0))
|
||||
|
@ -502,25 +574,42 @@ def guiMain(args=None):
|
|||
fastMenuLabel2 = Label(fastMenuFrame2, text='Menu speed')
|
||||
fastMenuLabel2.pack(side=LEFT)
|
||||
|
||||
owPalettesFrame2 = Frame(drowDownFrame2)
|
||||
owPalettesOptionMenu2 = OptionMenu(owPalettesFrame2, owPalettesVar, 'default', 'random', 'blackout')
|
||||
owPalettesOptionMenu2.pack(side=RIGHT)
|
||||
owPalettesLabel2 = Label(owPalettesFrame2, text='Overworld palettes')
|
||||
owPalettesLabel2.pack(side=LEFT)
|
||||
|
||||
uwPalettesFrame2 = Frame(drowDownFrame2)
|
||||
uwPalettesOptionMenu2 = OptionMenu(uwPalettesFrame2, uwPalettesVar, 'default', 'random', 'blackout')
|
||||
uwPalettesOptionMenu2.pack(side=RIGHT)
|
||||
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
||||
uwPalettesLabel2.pack(side=LEFT)
|
||||
|
||||
heartbeepFrame2.pack(expand=True, anchor=E)
|
||||
heartcolorFrame2.pack(expand=True, anchor=E)
|
||||
fastMenuFrame2.pack(expand=True, anchor=E)
|
||||
owPalettesFrame2.pack(expand=True, anchor=E)
|
||||
uwPalettesFrame2.pack(expand=True, anchor=E)
|
||||
|
||||
bottomFrame2 = Frame(topFrame2)
|
||||
|
||||
def adjustRom():
|
||||
guiargs = Namespace
|
||||
guiargs = Namespace()
|
||||
guiargs.heartbeep = heartbeepVar.get()
|
||||
guiargs.heartcolor = heartcolorVar.get()
|
||||
guiargs.fastmenu = fastMenuVar.get()
|
||||
guiargs.ow_palettes = owPalettesVar.get()
|
||||
guiargs.uw_palettes = uwPalettesVar.get()
|
||||
guiargs.quickswap = bool(quickSwapVar.get())
|
||||
guiargs.disablemusic = bool(disableMusicVar.get())
|
||||
guiargs.rom = romVar2.get()
|
||||
guiargs.baserom = romVar.get()
|
||||
guiargs.sprite = sprite
|
||||
try:
|
||||
adjust(args=guiargs)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||
else:
|
||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||
|
@ -553,19 +642,19 @@ def guiMain(args=None):
|
|||
|
||||
bowFrame = Frame(itemList1)
|
||||
bowLabel = Label(bowFrame, text='Bow')
|
||||
bowVar = StringVar(value='1')
|
||||
bowVar = StringVar(value='0')
|
||||
bowEntry = Entry(bowFrame, textvariable=bowVar, width=3, validate='all', vcmd=vcmd)
|
||||
bowFrame.pack()
|
||||
bowLabel.pack(anchor=W, side=LEFT, padx=(0,53))
|
||||
bowEntry.pack(anchor=E)
|
||||
|
||||
silverarrowFrame = Frame(itemList1)
|
||||
silverarrowLabel = Label(silverarrowFrame, text='Silver Arrow')
|
||||
silverarrowVar = StringVar(value='1')
|
||||
silverarrowEntry = Entry(silverarrowFrame, textvariable=silverarrowVar, width=3, validate='all', vcmd=vcmd)
|
||||
silverarrowFrame.pack()
|
||||
silverarrowLabel.pack(anchor=W, side=LEFT, padx=(0,13))
|
||||
silverarrowEntry.pack(anchor=E)
|
||||
progbowFrame = Frame(itemList1)
|
||||
progbowLabel = Label(progbowFrame, text='Prog.Bow')
|
||||
progbowVar = StringVar(value='2')
|
||||
progbowEntry = Entry(progbowFrame, textvariable=progbowVar, width=3, validate='all', vcmd=vcmd)
|
||||
progbowFrame.pack()
|
||||
progbowLabel.pack(anchor=W, side=LEFT, padx=(0,25))
|
||||
progbowEntry.pack(anchor=E)
|
||||
|
||||
boomerangFrame = Frame(itemList1)
|
||||
boomerangLabel = Label(boomerangFrame, text='Boomerang')
|
||||
|
@ -921,7 +1010,7 @@ def guiMain(args=None):
|
|||
|
||||
bcap5Frame = Frame(itemList3)
|
||||
bcap5Label = Label(bcap5Frame, text='Bomb C.+5')
|
||||
bcap5Var = StringVar(value='6')
|
||||
bcap5Var = StringVar(value='0')
|
||||
bcap5Entry = Entry(bcap5Frame, textvariable=bcap5Var, width=3, validate='all', vcmd=vcmd)
|
||||
bcap5Frame.pack()
|
||||
bcap5Label.pack(anchor=W, side=LEFT, padx=(0,16))
|
||||
|
@ -929,7 +1018,7 @@ def guiMain(args=None):
|
|||
|
||||
bcap10Frame = Frame(itemList3)
|
||||
bcap10Label = Label(bcap10Frame, text='Bomb C.+10')
|
||||
bcap10Var = StringVar(value='1')
|
||||
bcap10Var = StringVar(value='0')
|
||||
bcap10Entry = Entry(bcap10Frame, textvariable=bcap10Var, width=3, validate='all', vcmd=vcmd)
|
||||
bcap10Frame.pack()
|
||||
bcap10Label.pack(anchor=W, side=LEFT, padx=(0,10))
|
||||
|
@ -937,7 +1026,7 @@ def guiMain(args=None):
|
|||
|
||||
acap5Frame = Frame(itemList4)
|
||||
acap5Label = Label(acap5Frame, text='Arrow C.+5')
|
||||
acap5Var = StringVar(value='6')
|
||||
acap5Var = StringVar(value='0')
|
||||
acap5Entry = Entry(acap5Frame, textvariable=acap5Var, width=3, validate='all', vcmd=vcmd)
|
||||
acap5Frame.pack()
|
||||
acap5Label.pack(anchor=W, side=LEFT, padx=(0,7))
|
||||
|
@ -945,7 +1034,7 @@ def guiMain(args=None):
|
|||
|
||||
acap10Frame = Frame(itemList4)
|
||||
acap10Label = Label(acap10Frame, text='Arrow C.+10')
|
||||
acap10Var = StringVar(value='1')
|
||||
acap10Var = StringVar(value='0')
|
||||
acap10Entry = Entry(acap10Frame, textvariable=acap10Var, width=3, validate='all', vcmd=vcmd)
|
||||
acap10Frame.pack()
|
||||
acap10Label.pack(anchor=W, side=LEFT, padx=(0,1))
|
||||
|
@ -961,7 +1050,7 @@ def guiMain(args=None):
|
|||
|
||||
arrow10Frame = Frame(itemList4)
|
||||
arrow10Label = Label(arrow10Frame, text='Arrows (10)')
|
||||
arrow10Var = StringVar(value='5')
|
||||
arrow10Var = StringVar(value='12')
|
||||
arrow10Entry = Entry(arrow10Frame, textvariable=arrow10Var, width=3, validate='all', vcmd=vcmd)
|
||||
arrow10Frame.pack()
|
||||
arrow10Label.pack(anchor=W, side=LEFT, padx=(0,7))
|
||||
|
@ -977,12 +1066,20 @@ def guiMain(args=None):
|
|||
|
||||
bomb3Frame = Frame(itemList4)
|
||||
bomb3Label = Label(bomb3Frame, text='Bombs (3)')
|
||||
bomb3Var = StringVar(value='10')
|
||||
bomb3Var = StringVar(value='16')
|
||||
bomb3Entry = Entry(bomb3Frame, textvariable=bomb3Var, width=3, validate='all', vcmd=vcmd)
|
||||
bomb3Frame.pack()
|
||||
bomb3Label.pack(anchor=W, side=LEFT, padx=(0,13))
|
||||
bomb3Entry.pack(anchor=E)
|
||||
|
||||
bomb10Frame = Frame(itemList4)
|
||||
bomb10Label = Label(bomb10Frame, text='Bombs (10)')
|
||||
bomb10Var = StringVar(value='1')
|
||||
bomb10Entry = Entry(bomb10Frame, textvariable=bomb10Var, width=3, validate='all', vcmd=vcmd)
|
||||
bomb10Frame.pack()
|
||||
bomb10Label.pack(anchor=W, side=LEFT, padx=(0,7))
|
||||
bomb10Entry.pack(anchor=E)
|
||||
|
||||
rupee1Frame = Frame(itemList4)
|
||||
rupee1Label = Label(rupee1Frame, text='Rupee (1)')
|
||||
rupee1Var = StringVar(value='2')
|
||||
|
@ -1031,14 +1128,6 @@ def guiMain(args=None):
|
|||
rupee300Label.pack(anchor=W, side=LEFT, padx=(0,0))
|
||||
rupee300Entry.pack(anchor=E)
|
||||
|
||||
rupoorFrame = Frame(itemList4)
|
||||
rupoorLabel = Label(rupoorFrame, text='Rupoor')
|
||||
rupoorVar = StringVar(value='0')
|
||||
rupoorEntry = Entry(rupoorFrame, textvariable=rupoorVar, width=3, validate='all', vcmd=vcmd)
|
||||
rupoorFrame.pack()
|
||||
rupoorLabel.pack(anchor=W, side=LEFT, padx=(0,28))
|
||||
rupoorEntry.pack(anchor=E)
|
||||
|
||||
blueclockFrame = Frame(itemList4)
|
||||
blueclockLabel = Label(blueclockFrame, text='Blue Clock')
|
||||
blueclockVar = StringVar(value='0')
|
||||
|
@ -1063,6 +1152,14 @@ def guiMain(args=None):
|
|||
redclockLabel.pack(anchor=W, side=LEFT, padx=(0,14))
|
||||
redclockEntry.pack(anchor=E)
|
||||
|
||||
silverarrowFrame = Frame(itemList5)
|
||||
silverarrowLabel = Label(silverarrowFrame, text='Silver Arrow')
|
||||
silverarrowVar = StringVar(value='0')
|
||||
silverarrowEntry = Entry(silverarrowFrame, textvariable=silverarrowVar, width=3, validate='all', vcmd=vcmd)
|
||||
silverarrowFrame.pack()
|
||||
silverarrowLabel.pack(anchor=W, side=LEFT, padx=(0,64))
|
||||
silverarrowEntry.pack(anchor=E)
|
||||
|
||||
universalkeyFrame = Frame(itemList5)
|
||||
universalkeyLabel = Label(universalkeyFrame, text='Universal Key')
|
||||
universalkeyVar = StringVar(value='0')
|
||||
|
@ -1095,6 +1192,14 @@ def guiMain(args=None):
|
|||
triforceLabel.pack(anchor=W, side=LEFT, padx=(0,23))
|
||||
triforceEntry.pack(anchor=E)
|
||||
|
||||
rupoorFrame = Frame(itemList5)
|
||||
rupoorLabel = Label(rupoorFrame, text='Rupoor')
|
||||
rupoorVar = StringVar(value='0')
|
||||
rupoorEntry = Entry(rupoorFrame, textvariable=rupoorVar, width=3, validate='all', vcmd=vcmd)
|
||||
rupoorFrame.pack()
|
||||
rupoorLabel.pack(anchor=W, side=LEFT, padx=(0,87))
|
||||
rupoorEntry.pack(anchor=E)
|
||||
|
||||
rupoorcostFrame = Frame(itemList5)
|
||||
rupoorcostLabel = Label(rupoorcostFrame, text='Rupoor Cost')
|
||||
rupoorcostVar = StringVar(value='10')
|
||||
|
@ -1111,13 +1216,17 @@ def guiMain(args=None):
|
|||
topFrame3.pack(side=TOP, pady=(17,0))
|
||||
|
||||
if args is not None:
|
||||
for k,v in vars(args).items():
|
||||
if type(v) is dict:
|
||||
setattr(args, k, v[1]) # only get values for player 1 for now
|
||||
# load values from commandline args
|
||||
createSpoilerVar.set(int(args.create_spoiler))
|
||||
suppressRomVar.set(int(args.suppress_rom))
|
||||
keysanityVar.set(args.keysanity)
|
||||
mapshuffleVar.set(args.mapshuffle)
|
||||
compassshuffleVar.set(args.compassshuffle)
|
||||
keyshuffleVar.set(args.keyshuffle)
|
||||
bigkeyshuffleVar.set(args.bigkeyshuffle)
|
||||
retroVar.set(args.retro)
|
||||
if args.nodungeonitems:
|
||||
dungeonItemsVar.set(int(not args.nodungeonitems))
|
||||
quickSwapVar.set(int(args.quickswap))
|
||||
disableMusicVar.set(int(args.disablemusic))
|
||||
if args.count:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import collections
|
||||
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
|
||||
from BaseClasses import RegionType
|
||||
from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region
|
||||
|
||||
|
||||
def create_inverted_regions(world, player):
|
||||
|
@ -302,52 +303,13 @@ def create_inverted_regions(world, player):
|
|||
create_cave_region(player, 'The Sky', 'A Dark Sky', None, ['DDM Landing','NEDW Landing', 'WDW Landing', 'SDW Landing', 'EDW Landing', 'DD Landing', 'DLHL Landing'])
|
||||
]
|
||||
|
||||
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, (item, price) in enumerate(default_shop_contents[region_name]):
|
||||
shop.add_inventory(index, item, price)
|
||||
world.initialize_regions()
|
||||
|
||||
region = world.get_region('Capacity Upgrade', player)
|
||||
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
|
||||
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
|
||||
world.intialize_regions()
|
||||
|
||||
def create_lw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
def create_dw_region(player, name, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
|
||||
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
|
||||
|
||||
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
|
||||
|
||||
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
|
||||
ret = Region(name, type, hint, player)
|
||||
if locations is None:
|
||||
locations = []
|
||||
if exits is None:
|
||||
exits = []
|
||||
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
for location in locations:
|
||||
address, crystal, hint_text = location_table[location]
|
||||
ret.locations.append(Location(player, location, address, crystal, hint_text, ret))
|
||||
return ret
|
||||
|
||||
def mark_dark_world_regions(world):
|
||||
def mark_dark_world_regions(world, player):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
# That is ok. the bunny logic will check for this case and incorporate special rules.
|
||||
queue = collections.deque(region for region in world.regions if region.type == RegionType.DarkWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
@ -360,7 +322,7 @@ def mark_dark_world_regions(world):
|
|||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in world.regions if region.type == RegionType.LightWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
@ -372,270 +334,3 @@ def mark_dark_world_regions(world):
|
|||
if exit.connected_region not in seen:
|
||||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
# (room_id, shopkeeper, replaceable)
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
|
||||
'Red Shield Shop': (0x0110, 0xC1, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
|
||||
'Village of Outcasts Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Potion Shop': (0x010F, 0xC1, True),
|
||||
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
|
||||
'Kakariko Shop': (0x011F, 0xA0, True),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
|
||||
'Potion Shop': (0x0109, 0xFF, False),
|
||||
# Bomb Shop not currently modeled as a shop, due to special nature of items
|
||||
}
|
||||
# region, [item]
|
||||
# slot, item, price, max=0, replacement=None, replacement_price=0
|
||||
# item = (item, price)
|
||||
|
||||
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
|
||||
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
default_shop_contents = {
|
||||
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
|
||||
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
|
||||
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
|
||||
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
|
||||
'Village of Outcasts Shop': _dark_world_shop_defaults,
|
||||
'Dark World Potion Shop': _dark_world_shop_defaults,
|
||||
'Light World Death Mountain Shop': _basic_shop_defaults,
|
||||
'Kakariko Shop': _basic_shop_defaults,
|
||||
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2EB18, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014A, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, False, 'underwater'),
|
||||
'Purple Chest': (0x33D68, False, 'from a box'),
|
||||
'Blind\'s Hideout - Top': (0xEB0F, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Left': (0xEB12, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Right': (0xEB15, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Far Left': (0xEB18, False, 'in a basement'),
|
||||
'Blind\'s Hideout - Far Right': (0xEB1B, False, 'in a basement'),
|
||||
'Link\'s Uncle': (0x2DF45, False, 'with your uncle'),
|
||||
'Secret Passage': (0xE971, False, 'near your uncle'),
|
||||
'King Zora': (0xEE1C3, False, 'at a high price'),
|
||||
'Zora\'s Ledge': (0x180149, False, 'near Zora'),
|
||||
'Waterfall Fairy - Left': (0xE9B0, False, 'near a fairy'),
|
||||
'Waterfall Fairy - Right': (0xE9D1, False, 'near a fairy'),
|
||||
'King\'s Tomb': (0xE97A, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xE98C, False, 'in the dam'),
|
||||
'Link\'s House': (0xE9BC, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xE9CE, False, 'in the bar'),
|
||||
'Chicken House': (0xE9E9, False, 'near poultry'),
|
||||
'Aginah\'s Cave': (0xE9F2, False, 'with Aginah'),
|
||||
'Sahasrahla\'s Hut - Left': (0xEA82, False, 'near the elder'),
|
||||
'Sahasrahla\'s Hut - Middle': (0xEA85, False, 'near the elder'),
|
||||
'Sahasrahla\'s Hut - Right': (0xEA88, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2F1FC, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xEA8E, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xEA91, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xEA94, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xEA97, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xEA9A, False, 'in a well'),
|
||||
'Blacksmith': (0x18002A, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, False, 'with the bat'),
|
||||
'Sick Kid': (0x339CF, False, 'with the sick'),
|
||||
'Hobo': (0x33E7D, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xEB42, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xEB45, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xEB48, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xEB4B, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xEB4E, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xEB3F, False, 'alone in a cave'),
|
||||
'Library': (0x180012, False, 'near books'),
|
||||
'Potion Shop': (0x180014, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, False, 'on an island'),
|
||||
'Maze Race': (0x180142, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xE98F, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xE9B6, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xE9CB, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xE9C2, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xE977, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xE97D, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xE9B3, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xE9B9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xE9F5, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289B0, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xE974, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xEB0C, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Zelda\'s Chest': (0xEB09, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xE96E, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xEB5D, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xEB60, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xEB63, False, 'in the sewers'),
|
||||
'Sanctuary': (0xEA79, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xEAB5, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xEAB2, False, 'in Castle Tower'),
|
||||
'Old Man': (0xF69FA, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xEB2A, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xEB2D, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Right': (0xEB30, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Far Right': (0xEB33, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xEB36, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xEB39, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xEB3C, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xE9BF, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xE9AD, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xE9E6, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xE9FB, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xE9F8, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, False, 'on the pyramid'),
|
||||
'Catfish': (0xEE185, False, 'with a catfish'),
|
||||
'Stumpy': (0x330C7, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xEB1E, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xEB21, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xEB24, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xEB27, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xE980, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xE983, False, 'near a fairy'),
|
||||
'Brewery': (0xE9EC, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xE9EF, False, 'alone in a home'),
|
||||
'Chest Game': (0xEDA8, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xEA73, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xEA76, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xEA7C, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xEA7F, False, 'in a connection'),
|
||||
'Spike Cave': (0xEA8B, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xEB51, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xEB54, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xEB5A, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xEB57, False, 'across pits'),
|
||||
'Floating Island': (0x180141, False, 'on an island'),
|
||||
'Mimic Cave': (0xE9C5, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xEA9D, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xE986, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xE989, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xEAA0, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xEAA6, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xEAA3, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xEAA9, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xEAAC, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xEAAF, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'),
|
||||
'Thieves\' Town - Big Key Chest': (0xEA04, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Map Chest': (0xEA01, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Compass Chest': (0xEA07, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Ambush Chest': (0xEA0A, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Attic': (0xEA0D, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Big Chest': (0xEA10, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Blind\'s Cell': (0xEA13, False, 'in Thieves\' Town'),
|
||||
'Thieves\' Town - Boss': (0x180156, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xE992, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xE99B, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xE998, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xE9A1, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xE9C8, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xE99E, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xE9FE, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xE9D4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xE995, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xE9AA, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xE9E3, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xE9E0, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xE9A4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xE9DD, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xEA67, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xEA6A, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xEA5E, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xEA61, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xE9DA, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xEA64, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xEA6D, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xEA22, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xEA1C, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xEA1F, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xEA16, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xEA25, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xEA19, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xEA34, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xEA31, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xEA2E, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xEA2B, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xEA28, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xEA5B, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xEA3D, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xEA49, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xEA37, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xEA3A, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xEA52, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xEA43, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xEA4C, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xEA4F, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xEA55, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xEA58, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xEA40, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xEA46, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'),
|
||||
'Ganons Tower - Bob\'s Torch': (0x180161, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Hope Room - Left': (0xEAD9, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Hope Room - Right': (0xEADC, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Tile Room': (0xEAE2, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xEAE5, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xEAE8, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xEAEB, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xEAEE, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xEAB8, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xEABB, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xEABE, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xEAC1, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Map Chest': (0xEAD3, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Firesnake Room': (0xEAD0, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xEAC4, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xEAC7, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xEACA, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xEACD, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Bob\'s Chest': (0xEADF, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Chest': (0xEAD6, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Key Room - Left': (0xEAF4, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Key Room - Right': (0xEAF7, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Big Key Chest': (0xEAF1, False, 'in Ganon\'s Tower'),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xEAFD, False, 'atop Ganon\'s Tower'),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xEB00, False, 'atop Ganon\'s Tower'),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xEB03, False, 'atop Ganon\'s Tower'),
|
||||
'Ganons Tower - Validation Chest': (0xEB06, False, 'atop Ganon\'s Tower'),
|
||||
'Ganon': (None, False, 'from me'),
|
||||
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'),
|
||||
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'),
|
||||
'Floodgate': (None, False, None),
|
||||
'Frog': (None, False, None),
|
||||
'Missing Smith': (None, False, None),
|
||||
'Dark Blacksmith Ruins': (None, False, None),
|
||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'),
|
||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'),
|
||||
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'),
|
||||
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'),
|
||||
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'),
|
||||
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'),
|
||||
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')}
|
||||
|
|
300
ItemList.py
300
ItemList.py
|
@ -13,7 +13,7 @@ from Items import ItemFactory
|
|||
#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.
|
||||
|
||||
alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp',
|
||||
alwaysitems = ['Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Flute', 'Hammer', 'Hookshot', 'Ice Rod', 'Lamp',
|
||||
'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel', 'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang']
|
||||
progressivegloves = ['Progressive Glove'] * 2
|
||||
basicgloves = ['Power Glove', 'Titans Mitts']
|
||||
|
@ -35,7 +35,7 @@ Difficulty = namedtuple('Difficulty',
|
|||
'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother',
|
||||
'triforcehunt', 'triforce_pieces_required', 'retro',
|
||||
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
|
||||
'progressive_armor_limit', 'progressive_bottle_limit',
|
||||
'progressive_armor_limit', 'progressive_bottle_limit',
|
||||
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
|
||||
|
||||
total_items_to_place = 153
|
||||
|
@ -51,8 +51,8 @@ difficulties = {
|
|||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Blue Mail', 'Red Mail'],
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
basicbow = ['Bow', 'Silver Arrows'],
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
|
@ -78,8 +78,8 @@ difficulties = {
|
|||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
basicbow = ['Bow'] * 2,
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
|
@ -105,8 +105,8 @@ difficulties = {
|
|||
progressivearmor = ['Progressive Armor'] * 2, # neither will count
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 3,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
basicbow = ['Bow'] * 2,
|
||||
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
|
@ -125,26 +125,30 @@ difficulties = {
|
|||
}
|
||||
|
||||
def generate_itempool(world, player):
|
||||
if (world.difficulty not in ['normal', 'hard', 'expert'] or world.goal not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
|
||||
or world.mode 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']):
|
||||
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal',
|
||||
'dungeons',
|
||||
'triforcehunt',
|
||||
'crystals']
|
||||
or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer[player] not in [False,
|
||||
'display',
|
||||
'timed',
|
||||
'timed-ohko',
|
||||
'ohko',
|
||||
'timed-countdown']):
|
||||
raise NotImplementedError('Not supported yet')
|
||||
if world.timer[player] in ['ohko', 'timed-ohko']:
|
||||
world.can_take_damage[player] = False
|
||||
|
||||
if world.timer in ['ohko', 'timed-ohko']:
|
||||
world.can_take_damage = False
|
||||
|
||||
if world.goal in ['pedestal', 'triforcehunt']:
|
||||
if world.goal[player] in ['pedestal', 'triforcehunt']:
|
||||
world.push_item(world.get_location('Ganon', player), ItemFactory('Nothing', player), False)
|
||||
else:
|
||||
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
|
||||
|
||||
if world.goal in ['triforcehunt']:
|
||||
if world.mode == 'inverted':
|
||||
region = world.get_region('Light World',player)
|
||||
else:
|
||||
region = world.get_region('Hyrule Castle Courtyard', player)
|
||||
|
||||
if world.goal[player] in ['triforcehunt']:
|
||||
region = world.get_region('Light World',player)
|
||||
|
||||
loc = Location(player, "Murahdahla", parent=region)
|
||||
loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count
|
||||
loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) > state.world.treasure_hunt_count[player]
|
||||
region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
|
@ -153,7 +157,7 @@ def generate_itempool(world, player):
|
|||
world.push_item(loc, ItemFactory('Triforce', player), False)
|
||||
loc.event = True
|
||||
loc.locked = True
|
||||
|
||||
|
||||
world.get_location('Ganon', player).event = True
|
||||
world.get_location('Ganon', player).locked = True
|
||||
world.push_item(world.get_location('Agahnim 1', player), ItemFactory('Beat Agahnim 1', player), False)
|
||||
|
@ -177,38 +181,89 @@ def generate_itempool(world, player):
|
|||
|
||||
# set up item pool
|
||||
if world.custom:
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro, world.customitemarray)
|
||||
world.rupoor_cost = min(world.customitemarray[67], 9999)
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
|
||||
lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive[player], world.shuffle[player],
|
||||
world.difficulty[player], world.timer[player], world.goal[player],
|
||||
world.mode[player], world.swords[player],
|
||||
world.retro[player], world.customitemarray)
|
||||
world.rupoor_cost = min(world.customitemarray[69], 9999)
|
||||
else:
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle, world.difficulty, world.timer, world.goal, world.mode, world.swords, world.retro)
|
||||
world.itempool += ItemFactory(pool, player)
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
|
||||
lamps_needed_for_dark_rooms) = get_pool_core(world.progressive[player], world.shuffle[player],
|
||||
world.difficulty[player], world.timer[player], world.goal[player],
|
||||
world.mode[player], world.swords[player], world.retro[player])
|
||||
|
||||
for item in precollected_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
for (location, item) in placed_items:
|
||||
|
||||
if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player):
|
||||
if "Link's Uncle" not in placed_items:
|
||||
found_sword = False
|
||||
found_bow = False
|
||||
possible_weapons = []
|
||||
for item in pool:
|
||||
if item in ['Progressive Sword', 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword']:
|
||||
if not found_sword and world.swords[player] != 'swordless':
|
||||
found_sword = True
|
||||
possible_weapons.append(item)
|
||||
if item in ['Progressive Bow', 'Bow'] and not found_bow:
|
||||
found_bow = True
|
||||
possible_weapons.append(item)
|
||||
if item in ['Hammer', 'Bombs (10)', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
|
||||
if item not in possible_weapons:
|
||||
possible_weapons.append(item)
|
||||
starting_weapon = random.choice(possible_weapons)
|
||||
placed_items["Link's Uncle"] = starting_weapon
|
||||
pool.remove(starting_weapon)
|
||||
if placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Cane of Somaria', 'Cane of Byrna'] and world.enemy_health[player] not in ['default', 'easy']:
|
||||
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
|
||||
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
|
||||
if clock_mode is not None:
|
||||
world.clock_mode = clock_mode
|
||||
if treasure_hunt_count is not None:
|
||||
world.treasure_hunt_count = treasure_hunt_count
|
||||
if treasure_hunt_icon is not None:
|
||||
world.treasure_hunt_icon = treasure_hunt_icon
|
||||
|
||||
if world.keysanity:
|
||||
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player])
|
||||
items = ItemFactory(pool, player)
|
||||
|
||||
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
|
||||
|
||||
if clock_mode is not None:
|
||||
world.clock_mode[player] = clock_mode
|
||||
|
||||
if treasure_hunt_count is not None:
|
||||
world.treasure_hunt_count[player] = treasure_hunt_count
|
||||
if treasure_hunt_icon is not None:
|
||||
world.treasure_hunt_icon[player] = treasure_hunt_icon
|
||||
|
||||
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
|
||||
and ((item.smallkey and world.keyshuffle[player])
|
||||
or (item.bigkey and world.bigkeyshuffle[player])
|
||||
or (item.map and world.mapshuffle[player])
|
||||
or (item.compass and world.compassshuffle[player]))])
|
||||
|
||||
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
|
||||
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
||||
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
||||
if world.difficulty in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
|
||||
[item for item in world.itempool if item.name == 'Boss Heart Container' and item.player == player][0].advancement = True
|
||||
elif world.difficulty in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
|
||||
adv_heart_pieces = [item for item in world.itempool if item.name == 'Piece of Heart' and item.player == player][0:4]
|
||||
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
|
||||
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
|
||||
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
|
||||
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
|
||||
for hp in adv_heart_pieces:
|
||||
hp.advancement = True
|
||||
|
||||
beeweights = {0: {None: 100},
|
||||
1: {None: 75, 'trap': 25},
|
||||
2: {None: 40, 'trap': 40, 'bee': 20},
|
||||
3: {'trap': 50, 'bee': 50},
|
||||
4: {'trap': 100}}
|
||||
def beemizer(item):
|
||||
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
|
||||
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
|
||||
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
|
||||
return item
|
||||
|
||||
world.itempool += [beemizer(item) for item in items]
|
||||
|
||||
# shuffle medallions
|
||||
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
|
@ -217,7 +272,7 @@ def generate_itempool(world, player):
|
|||
place_bosses(world, player)
|
||||
set_up_shops(world, player)
|
||||
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
set_up_take_anys(world, player)
|
||||
|
||||
create_dynamic_shop_locations(world, player)
|
||||
|
@ -233,9 +288,9 @@ take_any_locations = [
|
|||
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
|
||||
|
||||
def set_up_take_anys(world, player):
|
||||
if world.mode == 'inverted' and 'Dark Sanctuary Hint' in take_any_locations:
|
||||
if world.mode[player] == 'inverted' and 'Dark Sanctuary Hint' in take_any_locations:
|
||||
take_any_locations.remove('Dark Sanctuary Hint')
|
||||
|
||||
|
||||
regions = random.sample(take_any_locations, 5)
|
||||
|
||||
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
|
||||
|
@ -244,11 +299,10 @@ def set_up_take_anys(world, player):
|
|||
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance, old_man_take_any, player)
|
||||
connect_entrance(world, entrance.name, old_man_take_any.name, player)
|
||||
entrance.target = 0x58
|
||||
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True)
|
||||
old_man_take_any.shop = Shop(old_man_take_any, 0x0112, ShopType.TakeAny, 0xE2, True, True)
|
||||
world.shops.append(old_man_take_any.shop)
|
||||
old_man_take_any.shop.active = True
|
||||
|
||||
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
|
||||
if swords:
|
||||
|
@ -267,15 +321,14 @@ def set_up_take_anys(world, player):
|
|||
target, room_id = random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)])
|
||||
reg = regions.pop()
|
||||
entrance = world.get_region(reg, player).entrances[0]
|
||||
connect_entrance(world, entrance, take_any, player)
|
||||
connect_entrance(world, entrance.name, take_any.name, player)
|
||||
entrance.target = target
|
||||
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True)
|
||||
take_any.shop = Shop(take_any, room_id, ShopType.TakeAny, 0xE3, True, True)
|
||||
world.shops.append(take_any.shop)
|
||||
take_any.shop.active = True
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0)
|
||||
|
||||
world.intialize_regions()
|
||||
world.initialize_regions()
|
||||
|
||||
def create_dynamic_shop_locations(world, player):
|
||||
for shop in world.shops:
|
||||
|
@ -312,9 +365,9 @@ def fill_prizes(world, attempts=15):
|
|||
prize_locs = list(empty_crystal_locations)
|
||||
random.shuffle(prizepool)
|
||||
random.shuffle(prize_locs)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool)
|
||||
fill_restrictive(world, all_state, prize_locs, prizepool, True)
|
||||
except FillError as e:
|
||||
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts)
|
||||
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1)
|
||||
for location in empty_crystal_locations:
|
||||
location.item = None
|
||||
continue
|
||||
|
@ -324,30 +377,23 @@ def fill_prizes(world, attempts=15):
|
|||
|
||||
|
||||
def set_up_shops(world, player):
|
||||
# Changes to basic Shops
|
||||
# TODO: move hard+ mode changes for sheilds here, utilizing the new shops
|
||||
|
||||
for shop in world.shops:
|
||||
shop.active = True
|
||||
|
||||
if world.retro:
|
||||
if world.retro[player]:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
rss.active = True
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
|
||||
# Randomized changes to Shops
|
||||
if world.retro:
|
||||
for shop in random.sample([s for s in world.shops if s.replaceable and s.type == ShopType.Shop and s.region.player == player], 5):
|
||||
shop.active = True
|
||||
if not rss.locked:
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
for shop in random.sample([s for s in world.shops if s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player], 5):
|
||||
shop.locked = True
|
||||
shop.add_inventory(0, 'Single Arrow', 80)
|
||||
shop.add_inventory(1, 'Small Key (Universal)', 100)
|
||||
shop.add_inventory(2, 'Bombs (10)', 50)
|
||||
rss.locked = True
|
||||
|
||||
#special shop types
|
||||
|
||||
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro):
|
||||
pool = []
|
||||
placed_items = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
treasure_hunt_count = None
|
||||
|
@ -355,6 +401,10 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
|
||||
pool.extend(alwaysitems)
|
||||
|
||||
def place_item(loc, item):
|
||||
assert loc not in placed_items
|
||||
placed_items[loc] = item
|
||||
|
||||
def want_progressives():
|
||||
return random.choice([True, False]) if progressive == 'random' else progressive == 'on'
|
||||
|
||||
|
@ -366,9 +416,9 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
lamps_needed_for_dark_rooms = 1
|
||||
|
||||
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
|
||||
if shuffle == 'insanity_legacy':
|
||||
placed_items.append(('Link\'s House', 'Magic Mirror'))
|
||||
placed_items.append(('Sanctuary', 'Moon Pearl'))
|
||||
if shuffle == 'insanity_legacy':
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
else:
|
||||
pool.extend(['Magic Mirror', 'Moon Pearl'])
|
||||
|
||||
|
@ -399,50 +449,37 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if swords != 'swordless':
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
else:
|
||||
pool.extend(diff.basicbow)
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
elif swords != 'swordless':
|
||||
pool.extend(diff.basicbow)
|
||||
else:
|
||||
pool.extend(['Bow', 'Silver Arrows'])
|
||||
|
||||
if swords == 'swordless':
|
||||
pool.extend(diff.swordless)
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
else:
|
||||
pool.extend(['Bow', 'Silver Arrows'])
|
||||
elif swords == 'assured':
|
||||
precollected_items.append('Fighter Sword')
|
||||
if want_progressives():
|
||||
pool.extend(diff.progressivesword)
|
||||
pool.extend(['Rupees (100)'])
|
||||
else:
|
||||
pool.extend(diff.basicsword)
|
||||
pool.extend(['Rupees (100)'])
|
||||
elif swords == 'vanilla':
|
||||
swords_to_use = []
|
||||
if want_progressives():
|
||||
swords_to_use.extend(diff.progressivesword)
|
||||
swords_to_use.extend(['Progressive Sword'])
|
||||
else:
|
||||
swords_to_use.extend(diff.basicsword)
|
||||
swords_to_use.extend(['Fighter Sword'])
|
||||
swords_to_use = diff.progressivesword.copy() if want_progressives() else diff.basicsword.copy()
|
||||
random.shuffle(swords_to_use)
|
||||
|
||||
placed_items.append(('Link\'s Uncle', swords_to_use.pop()))
|
||||
placed_items.append(('Blacksmith', swords_to_use.pop()))
|
||||
placed_items.append(('Pyramid Fairy - Left', swords_to_use.pop()))
|
||||
place_item('Link\'s Uncle', swords_to_use.pop())
|
||||
place_item('Blacksmith', swords_to_use.pop())
|
||||
place_item('Pyramid Fairy - Left', swords_to_use.pop())
|
||||
if goal != 'pedestal':
|
||||
placed_items.append(('Master Sword Pedestal', swords_to_use.pop()))
|
||||
place_item('Master Sword Pedestal', swords_to_use.pop())
|
||||
else:
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
else:
|
||||
if want_progressives():
|
||||
pool.extend(diff.progressivesword)
|
||||
pool.extend(['Progressive Sword'])
|
||||
else:
|
||||
pool.extend(diff.basicsword)
|
||||
pool.extend(['Fighter Sword'])
|
||||
progressive_swords = want_progressives()
|
||||
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
|
||||
if swords == 'assured':
|
||||
if progressive_swords:
|
||||
precollected_items.append('Progressive Sword')
|
||||
pool.remove('Progressive Sword')
|
||||
else:
|
||||
precollected_items.append('Fighter Sword')
|
||||
pool.remove('Fighter Sword')
|
||||
pool.extend(['Rupees (50)'])
|
||||
|
||||
extraitems = total_items_to_place - len(pool) - len(placed_items)
|
||||
|
||||
|
@ -466,7 +503,7 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
extraitems -= len(extra)
|
||||
|
||||
if goal == 'pedestal' and swords != 'vanilla':
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
if retro:
|
||||
pool = [item.replace('Single Arrow','Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool]
|
||||
|
@ -475,30 +512,34 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
|
|||
pool.extend(diff.retro)
|
||||
if mode == 'standard':
|
||||
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
||||
placed_items.append((key_location, 'Small Key (Universal)'))
|
||||
place_item(key_location, 'Small Key (Universal)')
|
||||
else:
|
||||
pool.extend(['Small Key (Universal)'])
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
|
||||
|
||||
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray):
|
||||
pool = []
|
||||
placed_items = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
treasure_hunt_count = None
|
||||
treasure_hunt_icon = None
|
||||
|
||||
def place_item(loc, item):
|
||||
assert loc not in placed_items
|
||||
placed_items[loc] = item
|
||||
|
||||
# Correct for insanely oversized item counts and take initial steps to handle undersized pools.
|
||||
for x in range(0, 64):
|
||||
for x in range(0, 66):
|
||||
if customitemarray[x] > total_items_to_place:
|
||||
customitemarray[x] = total_items_to_place
|
||||
if customitemarray[66] > total_items_to_place:
|
||||
customitemarray[66] = total_items_to_place
|
||||
if customitemarray[68] > total_items_to_place:
|
||||
customitemarray[68] = total_items_to_place
|
||||
itemtotal = 0
|
||||
for x in range(0, 65):
|
||||
for x in range(0, 66):
|
||||
itemtotal = itemtotal + customitemarray[x]
|
||||
itemtotal = itemtotal + customitemarray[66]
|
||||
itemtotal = itemtotal + customitemarray[68]
|
||||
itemtotal = itemtotal + customitemarray[70]
|
||||
|
||||
pool.extend(['Bow'] * customitemarray[0])
|
||||
pool.extend(['Silver Arrows']* customitemarray[1])
|
||||
|
@ -515,7 +556,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
pool.extend(['Lamp'] * customitemarray[12])
|
||||
pool.extend(['Hammer'] * customitemarray[13])
|
||||
pool.extend(['Shovel'] * customitemarray[14])
|
||||
pool.extend(['Ocarina'] * customitemarray[15])
|
||||
pool.extend(['Flute'] * customitemarray[15])
|
||||
pool.extend(['Bug Catching Net'] * customitemarray[16])
|
||||
pool.extend(['Book of Mudora'] * customitemarray[17])
|
||||
pool.extend(['Cane of Somaria'] * customitemarray[19])
|
||||
|
@ -559,8 +600,10 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
pool.extend(['Blue Clock'] * customitemarray[61])
|
||||
pool.extend(['Green Clock'] * customitemarray[62])
|
||||
pool.extend(['Red Clock'] * customitemarray[63])
|
||||
pool.extend(['Triforce Piece'] * customitemarray[64])
|
||||
pool.extend(['Triforce'] * customitemarray[66])
|
||||
pool.extend(['Progressive Bow'] * customitemarray[64])
|
||||
pool.extend(['Bombs (10)'] * customitemarray[65])
|
||||
pool.extend(['Triforce Piece'] * customitemarray[66])
|
||||
pool.extend(['Triforce'] * customitemarray[68])
|
||||
|
||||
diff = difficulties[difficulty]
|
||||
|
||||
|
@ -575,12 +618,12 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
thisbottle = random.choice(diff.bottles)
|
||||
pool.append(thisbottle)
|
||||
|
||||
if customitemarray[64] > 0 or customitemarray[65] > 0:
|
||||
treasure_hunt_count = max(min(customitemarray[65], 99), 1) #To display, count must be between 1 and 99.
|
||||
if customitemarray[66] > 0 or customitemarray[67] > 0:
|
||||
treasure_hunt_count = max(min(customitemarray[67], 99), 1) #To display, count must be between 1 and 99.
|
||||
treasure_hunt_icon = 'Triforce Piece'
|
||||
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
|
||||
if (customitemarray[64] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[66] == 0):
|
||||
extrapieces = treasure_hunt_count - customitemarray[64]
|
||||
if (customitemarray[66] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[68] == 0):
|
||||
extrapieces = treasure_hunt_count - customitemarray[66]
|
||||
pool.extend(['Triforce Piece'] * extrapieces)
|
||||
itemtotal = itemtotal + extrapieces
|
||||
|
||||
|
@ -592,25 +635,25 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
clock_mode = 'ohko'
|
||||
|
||||
if goal == 'pedestal':
|
||||
placed_items.append(('Master Sword Pedestal', 'Triforce'))
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
itemtotal = itemtotal + 1
|
||||
|
||||
if mode == 'standard':
|
||||
if retro:
|
||||
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
||||
placed_items.append((key_location, 'Small Key (Universal)'))
|
||||
pool.extend(['Small Key (Universal)'] * max((customitemarray[68] - 1), 0))
|
||||
place_item(key_location, 'Small Key (Universal)')
|
||||
pool.extend(['Small Key (Universal)'] * max((customitemarray[70] - 1), 0))
|
||||
else:
|
||||
pool.extend(['Small Key (Universal)'] * customitemarray[68])
|
||||
pool.extend(['Small Key (Universal)'] * customitemarray[70])
|
||||
else:
|
||||
pool.extend(['Small Key (Universal)'] * customitemarray[68])
|
||||
pool.extend(['Small Key (Universal)'] * customitemarray[70])
|
||||
|
||||
pool.extend(['Fighter Sword'] * customitemarray[32])
|
||||
pool.extend(['Progressive Sword'] * customitemarray[36])
|
||||
|
||||
if shuffle == 'insanity_legacy':
|
||||
placed_items.append(('Link\'s House', 'Magic Mirror'))
|
||||
placed_items.append(('Sanctuary', 'Moon Pearl'))
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
pool.extend(['Magic Mirror'] * max((customitemarray[22] -1 ), 0))
|
||||
pool.extend(['Moon Pearl'] * max((customitemarray[28] - 1), 0))
|
||||
else:
|
||||
|
@ -628,13 +671,14 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
def test():
|
||||
for difficulty in ['normal', 'hard', 'expert']:
|
||||
for goal in ['ganon', 'triforcehunt', 'pedestal']:
|
||||
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
|
||||
for timer in [False, 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
|
||||
for mode in ['open', 'standard', 'inverted']:
|
||||
for swords in ['random', 'assured', 'swordless', 'vanilla']:
|
||||
for progressive in ['on', 'off']:
|
||||
for shuffle in ['full', 'insanity_legacy']:
|
||||
for retro in [True, False]:
|
||||
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro)
|
||||
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords,
|
||||
retro)
|
||||
count = len(out[0]) + len(out[1])
|
||||
|
||||
correct_count = total_items_to_place
|
||||
|
|
44
Items.py
44
Items.py
|
@ -25,11 +25,12 @@ def ItemFactory(items, player):
|
|||
# Format: Name: (Advancement, Priority, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
||||
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
||||
'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||
'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
|
||||
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'),
|
||||
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
|
||||
'Magic Mirror': (True, False, None, 0x1A, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'),
|
||||
'Ocarina': (True, False, None, 0x14, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the Flute'),
|
||||
'Flute': (True, False, None, 0x14, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'flute boy plays again', 'the Flute'),
|
||||
'Pegasus Boots': (True, False, None, 0x4B, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'),
|
||||
'Power Glove': (True, False, None, 0x1B, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the glove'),
|
||||
'Cape': (True, False, None, 0x19, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'),
|
||||
|
@ -43,14 +44,14 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'),
|
||||
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'),
|
||||
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'),
|
||||
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
|
||||
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
|
||||
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
|
||||
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
|
||||
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'),
|
||||
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'),
|
||||
'Bottle (Green Potion)': (True, False, None, 0x2C, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a Bottle'),
|
||||
'Bottle (Blue Potion)': (True, False, None, 0x2D, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a Bottle'),
|
||||
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'),
|
||||
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'),
|
||||
'Bottle (Bee)': (True, False, None, 0x3C, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a Bottle'),
|
||||
'Bottle (Good Bee)': (True, False, None, 0x48, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a Bottle'),
|
||||
'Master Sword': (True, False, 'Sword', 0x50, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'),
|
||||
|
@ -60,19 +61,19 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Progressive Sword': (True, False, 'Sword', 0x5E, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a sword'),
|
||||
'Progressive Glove': (True, False, None, 0x61, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a glove'),
|
||||
'Silver Arrows': (True, False, None, 0x58, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the silver arrows'),
|
||||
'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01], None, None, None, None, None, None, None),
|
||||
'Red Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02], None, None, None, None, None, None, None),
|
||||
'Blue Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03], None, None, None, None, None, None, None),
|
||||
'Green Pendant': (True, False, 'Crystal', (0x04, 0x38, 0x62, 0x00, 0x69, 0x01), None, None, None, None, None, None, None),
|
||||
'Red Pendant': (True, False, 'Crystal', (0x02, 0x34, 0x60, 0x00, 0x69, 0x02), None, None, None, None, None, None, None),
|
||||
'Blue Pendant': (True, False, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, None),
|
||||
'Triforce': (True, False, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
|
||||
'Power Star': (True, False, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
|
||||
'Triforce Piece': (True, False, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
|
||||
'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06], None, None, None, None, None, None, None),
|
||||
'Crystal 1': (True, False, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, None),
|
||||
'Crystal 2': (True, False, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, None),
|
||||
'Crystal 3': (True, False, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, None),
|
||||
'Crystal 4': (True, False, 'Crystal', (0x20, 0x34, 0x64, 0x40, 0x6D, 0x06), None, None, None, None, None, None, None),
|
||||
'Crystal 5': (True, False, 'Crystal', (0x04, 0x32, 0x64, 0x40, 0x6E, 0x06), None, None, None, None, None, None, None),
|
||||
'Crystal 6': (True, False, 'Crystal', (0x01, 0x32, 0x64, 0x40, 0x6F, 0x06), None, None, None, None, None, None, None),
|
||||
'Crystal 7': (True, False, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, None),
|
||||
'Single Arrow': (False, False, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
|
||||
'Arrows (10)': (False, False, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'),
|
||||
'Arrow Upgrade (+10)': (False, False, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
|
||||
|
@ -161,15 +162,20 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
|
||||
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
||||
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
|
||||
'Red Potion': (False, False, None, 0x2E, None, None, None, None, None, None, None),
|
||||
'Green Potion': (False, False, None, 0x2F, None, None, None, None, None, None, None),
|
||||
'Blue Potion': (False, False, None, 0x30, None, None, None, None, None, None, None),
|
||||
'Bee': (False, False, None, 0x0E, None, None, None, None, None, None, None),
|
||||
'Small Heart': (False, False, None, 0x42, None, None, None, None, None, None, None),
|
||||
'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'friendship'),
|
||||
'Red Potion': (False, False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
|
||||
'Green Potion': (False, False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
|
||||
'Blue Potion': (False, False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
|
||||
'Bee': (False, False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'),
|
||||
'Small Heart': (False, False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
|
||||
'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||
'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||
'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||
'Return Smith': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||
'Pick Up Purple Chest': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||
'Open Floodgate': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||
}
|
||||
}
|
||||
|
||||
lookup_id_to_name = {data[3]: name for name, data in item_table.items()}
|
||||
|
||||
hint_blacklist = {"Triforce"}
|
1
LICENSE
1
LICENSE
|
@ -1,6 +1,7 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2020 Berserker66
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
320
Main.py
320
Main.py
|
@ -6,25 +6,33 @@ import logging
|
|||
import os
|
||||
import random
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
||||
from Regions import create_regions, mark_light_world_regions
|
||||
from Items import ItemFactory
|
||||
from Regions import create_regions, create_shops, mark_light_world_regions
|
||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||
from Rom import patch_rom, get_enemizer_patch, apply_rom_settings, Sprite, LocalRom, JsonRom
|
||||
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
||||
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes
|
||||
from Utils import output_path
|
||||
from Utils import output_path, parse_player_names
|
||||
|
||||
__version__ = '0.6.3-pre'
|
||||
|
||||
def main(args, seed=None):
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
# initialize the world
|
||||
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints)
|
||||
world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty,
|
||||
args.item_functionality, args.timer, args.progressive.copy(), args.goal, args.algorithm,
|
||||
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
|
||||
logger = logging.getLogger('')
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
|
@ -33,36 +41,65 @@ def main(args, seed=None):
|
|||
world.seed = int(seed)
|
||||
random.seed(world.seed)
|
||||
|
||||
world.crystals_needed_for_ganon = random.randint(0, 7) if args.crystals_ganon == 'random' else int(args.crystals_ganon)
|
||||
world.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt)
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
||||
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
|
||||
world.open_pyramid = args.openpyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.shuffleenemies.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.shufflepots = args.shufflepots.copy()
|
||||
world.progressive = args.progressive.copy()
|
||||
world.extendedmsu = args.extendedmsu.copy()
|
||||
|
||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||
|
||||
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
|
||||
logger.info('ALttP Entrance Randomizer Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
world.difficulty_requirements = difficulties[world.difficulty]
|
||||
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||
world.teams = len(parsed_names)
|
||||
for i, team in enumerate(parsed_names, 1):
|
||||
if world.players > 1:
|
||||
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
|
||||
for player, name in enumerate(team, 1):
|
||||
world.player_names[player].append(name)
|
||||
|
||||
if world.mode != 'inverted':
|
||||
for player in range(1, world.players + 1):
|
||||
logger.info('')
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||
|
||||
if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none':
|
||||
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
|
||||
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
item = ItemFactory(tok.strip(), player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
create_dungeons(world, player)
|
||||
else:
|
||||
for player in range(1, world.players + 1):
|
||||
else:
|
||||
create_inverted_regions(world, player)
|
||||
create_dungeons(world, player)
|
||||
create_shops(world, player)
|
||||
create_dungeons(world, player)
|
||||
|
||||
logger.info('Shuffling the World about.')
|
||||
|
||||
if world.mode != 'inverted':
|
||||
for player in range(1, world.players + 1):
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] != 'inverted':
|
||||
link_entrances(world, player)
|
||||
|
||||
mark_light_world_regions(world)
|
||||
else:
|
||||
for player in range(1, world.players + 1):
|
||||
mark_light_world_regions(world, player)
|
||||
else:
|
||||
link_inverted_entrances(world, player)
|
||||
|
||||
mark_dark_world_regions(world)
|
||||
mark_dark_world_regions(world, player)
|
||||
|
||||
logger.info('Generating Item Pool.')
|
||||
|
||||
|
@ -81,7 +118,8 @@ def main(args, seed=None):
|
|||
logger.info('Placing Dungeon Items.')
|
||||
|
||||
shuffled_locations = None
|
||||
if args.algorithm in ['balanced', 'vt26'] or args.keysanity:
|
||||
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
||||
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
|
||||
shuffled_locations = world.get_unfilled_locations()
|
||||
random.shuffle(shuffled_locations)
|
||||
fill_dungeons_restrictive(world, shuffled_locations)
|
||||
|
@ -99,12 +137,12 @@ def main(args, seed=None):
|
|||
elif args.algorithm == 'freshness':
|
||||
distribute_items_staleness(world)
|
||||
elif args.algorithm == 'vt25':
|
||||
distribute_items_restrictive(world, 0)
|
||||
distribute_items_restrictive(world, False)
|
||||
elif args.algorithm == 'vt26':
|
||||
|
||||
distribute_items_restrictive(world, gt_filler(world), shuffled_locations)
|
||||
distribute_items_restrictive(world, True, shuffled_locations)
|
||||
elif args.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world, gt_filler(world))
|
||||
distribute_items_restrictive(world, True)
|
||||
|
||||
if world.players > 1:
|
||||
logger.info('Balancing multiworld progression.')
|
||||
|
@ -112,55 +150,105 @@ def main(args, seed=None):
|
|||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
if args.sprite is not None:
|
||||
if isinstance(args.sprite, Sprite):
|
||||
sprite = args.sprite
|
||||
else:
|
||||
sprite = Sprite(args.sprite)
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
outfilebase = 'ER_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "", world.seed)
|
||||
|
||||
use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots)
|
||||
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
rom_names = []
|
||||
jsonout = {}
|
||||
if not args.suppress_rom:
|
||||
if world.players > 1:
|
||||
raise NotImplementedError("Multiworld rom writes have not been implemented")
|
||||
|
||||
def _gen_rom(team: int, player: int):
|
||||
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
|
||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or args.shufflepots[player] or sprite_random_on_hit)
|
||||
|
||||
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom, extendedmsu=args.extendedmsu[player])
|
||||
|
||||
patch_rom(world, rom, player, team, use_enemizer)
|
||||
|
||||
if use_enemizer and (args.enemizercli or not args.jsonout):
|
||||
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player],
|
||||
sprite_random_on_hit, extendedmsu=args.extendedmsu[player])
|
||||
if not args.jsonout:
|
||||
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000, args.extendedmsu[player])
|
||||
|
||||
if args.race:
|
||||
patch_race_rom(rom)
|
||||
|
||||
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
||||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||
args.ow_palettes[player], args.uw_palettes[player])
|
||||
|
||||
if args.jsonout:
|
||||
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||
else:
|
||||
player = 1
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-keysanity'
|
||||
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]].count(True) == 1:
|
||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[
|
||||
player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
||||
|
||||
local_rom = None
|
||||
if args.jsonout:
|
||||
rom = JsonRom()
|
||||
else:
|
||||
if use_enemizer:
|
||||
local_rom = LocalRom(args.rom)
|
||||
rom = JsonRom()
|
||||
else:
|
||||
rom = LocalRom(args.rom)
|
||||
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
|
||||
if world.players > 1:
|
||||
outfilepname += f'_P{player}'
|
||||
if world.players > 1 or world.teams > 1:
|
||||
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][
|
||||
team] != 'Player %d' % player else ''
|
||||
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player],
|
||||
world.difficulty_adjustments[player],
|
||||
world.mode[player], world.goal[player],
|
||||
"" if world.timer[player] in [False,
|
||||
'display'] else "-" +
|
||||
world.timer[
|
||||
player],
|
||||
world.shuffle[player], world.algorithm,
|
||||
mcsb_name,
|
||||
"-retro" if world.retro[player] else "",
|
||||
"-prog_" + world.progressive[player] if
|
||||
world.progressive[player] in ['off',
|
||||
'random'] else "",
|
||||
"-nohints" if not world.hints[
|
||||
player] else "")) if not args.outputname else ''
|
||||
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||
rom.write_to_file(rompath)
|
||||
if args.create_diff:
|
||||
import Patch
|
||||
Patch.create_patch_file(rompath)
|
||||
return (player, team, list(rom.name))
|
||||
|
||||
patch_rom(world, player, rom)
|
||||
|
||||
enemizer_patch = []
|
||||
if use_enemizer:
|
||||
enemizer_patch = get_enemizer_patch(world, player, rom, args.rom, args.enemizercli, args.shuffleenemies, args.enemy_health, args.enemy_damage, args.shufflepalette, args.shufflepots)
|
||||
|
||||
if args.jsonout:
|
||||
jsonout['patch'] = rom.patches
|
||||
if use_enemizer:
|
||||
jsonout['enemizer' % player] = enemizer_patch
|
||||
else:
|
||||
if use_enemizer:
|
||||
local_rom.patch_enemizer(rom.patches, os.path.join(os.path.dirname(args.enemizercli), "enemizerBasePatch.json"), enemizer_patch)
|
||||
rom = local_rom
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, world.quickswap, world.fastmenu, world.disable_music, sprite)
|
||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||
|
||||
if args.create_spoiler and not args.jsonout:
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
if not args.suppress_rom:
|
||||
import concurrent.futures
|
||||
futures = []
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for team in range(world.teams):
|
||||
for player in range(1, world.players + 1):
|
||||
futures.append(pool.submit(_gen_rom, team, player))
|
||||
for future in futures:
|
||||
rom_name = future.result()
|
||||
rom_names.append(rom_name)
|
||||
multidata = zlib.compress(json.dumps({"names": parsed_names,
|
||||
"roms": rom_names,
|
||||
"remote_items": [player for player in range(1, world.players + 1) if
|
||||
world.remote_items[player]],
|
||||
"locations": [((location.address, location.player),
|
||||
(location.item.code, location.item.player))
|
||||
for location in world.get_filled_locations() if
|
||||
type(location.address) is int]
|
||||
}).encode("utf-8"))
|
||||
if args.jsonout:
|
||||
jsonout["multidata"] = list(multidata)
|
||||
else:
|
||||
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
|
||||
f.write(multidata)
|
||||
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
|
@ -176,44 +264,54 @@ def main(args, seed=None):
|
|||
|
||||
return world
|
||||
|
||||
def gt_filler(world):
|
||||
if world.goal == 'triforcehunt':
|
||||
return random.randint(15, 50)
|
||||
return random.randint(0, 15)
|
||||
|
||||
def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints)
|
||||
ret = World(world.players, world.shuffle, 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.retro, world.custom, world.customitemarray, world.hints)
|
||||
ret.teams = world.teams
|
||||
ret.player_names = copy.deepcopy(world.player_names)
|
||||
ret.remote_items = world.remote_items.copy()
|
||||
ret.required_medallions = world.required_medallions.copy()
|
||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||
ret.powder_patch_required = world.powder_patch_required.copy()
|
||||
ret.ganonstower_vanilla = world.ganonstower_vanilla.copy()
|
||||
ret.treasure_hunt_count = world.treasure_hunt_count
|
||||
ret.treasure_hunt_icon = world.treasure_hunt_icon
|
||||
ret.sewer_light_cone = world.sewer_light_cone
|
||||
ret.treasure_hunt_count = world.treasure_hunt_count.copy()
|
||||
ret.treasure_hunt_icon = world.treasure_hunt_icon.copy()
|
||||
ret.sewer_light_cone = world.sewer_light_cone.copy()
|
||||
ret.light_world_light_cone = world.light_world_light_cone
|
||||
ret.dark_world_light_cone = world.dark_world_light_cone
|
||||
ret.seed = world.seed
|
||||
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge
|
||||
ret.can_access_trock_front = world.can_access_trock_front
|
||||
ret.can_access_trock_big_chest = world.can_access_trock_big_chest
|
||||
ret.can_access_trock_middle = world.can_access_trock_middle
|
||||
ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy()
|
||||
ret.can_access_trock_front = world.can_access_trock_front.copy()
|
||||
ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy()
|
||||
ret.can_access_trock_middle = world.can_access_trock_middle.copy()
|
||||
ret.can_take_damage = world.can_take_damage
|
||||
ret.difficulty_requirements = world.difficulty_requirements
|
||||
ret.fix_fake_world = world.fix_fake_world
|
||||
ret.difficulty_requirements = world.difficulty_requirements.copy()
|
||||
ret.fix_fake_world = world.fix_fake_world.copy()
|
||||
ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt
|
||||
ret.mapshuffle = world.mapshuffle.copy()
|
||||
ret.compassshuffle = world.compassshuffle.copy()
|
||||
ret.keyshuffle = world.keyshuffle.copy()
|
||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
||||
ret.open_pyramid = world.open_pyramid.copy()
|
||||
ret.boss_shuffle = world.boss_shuffle.copy()
|
||||
ret.enemy_shuffle = world.enemy_shuffle.copy()
|
||||
ret.enemy_health = world.enemy_health.copy()
|
||||
ret.enemy_damage = world.enemy_damage.copy()
|
||||
ret.beemizer = world.beemizer.copy()
|
||||
ret.timer = world.timer.copy()
|
||||
ret.shufflepots = world.shufflepots.copy()
|
||||
ret.extendedmsu = world.extendedmsu.copy()
|
||||
|
||||
if world.mode != 'inverted':
|
||||
for player in range(1, world.players + 1):
|
||||
for player in range(1, world.players + 1):
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
else:
|
||||
for player in range(1, world.players + 1):
|
||||
else:
|
||||
create_inverted_regions(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
create_shops(ret, player)
|
||||
create_dungeons(ret, player)
|
||||
|
||||
copy_dynamic_regions_and_locations(world, ret)
|
||||
|
||||
|
@ -224,7 +322,6 @@ def copy_world(world):
|
|||
|
||||
for shop in world.shops:
|
||||
copied_shop = ret.get_region(shop.region.name, shop.region.player).shop
|
||||
copied_shop.active = shop.active
|
||||
copied_shop.inventory = copy.copy(shop.inventory)
|
||||
|
||||
# connect copied world
|
||||
|
@ -241,6 +338,7 @@ def copy_world(world):
|
|||
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
|
||||
ret.get_location(location.name, location.player).item = item
|
||||
item.location = ret.get_location(location.name, location.player)
|
||||
item.world = ret
|
||||
if location.event:
|
||||
ret.get_location(location.name, location.player).event = True
|
||||
if location.locked:
|
||||
|
@ -250,9 +348,11 @@ def copy_world(world):
|
|||
for item in world.itempool:
|
||||
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
|
||||
|
||||
for item in world.precollected_items:
|
||||
ret.push_precollected(ItemFactory(item.name, item.player))
|
||||
|
||||
# copy progress items in state
|
||||
ret.state.prog_items = world.state.prog_items.copy()
|
||||
ret.precollected_items = world.precollected_items.copy()
|
||||
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
|
@ -263,14 +363,14 @@ def copy_world(world):
|
|||
def copy_dynamic_regions_and_locations(world, ret):
|
||||
for region in world.dynamic_regions:
|
||||
new_reg = Region(region.name, region.type, region.hint_text, region.player)
|
||||
new_reg.world = ret
|
||||
ret.regions.append(new_reg)
|
||||
ret.initialize_regions([new_reg])
|
||||
ret.dynamic_regions.append(new_reg)
|
||||
|
||||
# Note: ideally exits should be copied here, but the current use case (Take anys) do not require this
|
||||
|
||||
if region.shop:
|
||||
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.replaceable)
|
||||
new_reg.shop = Shop(new_reg, region.shop.room_id, region.shop.type, region.shop.shopkeeper_config, region.shop.custom, region.shop.locked)
|
||||
ret.shops.append(new_reg.shop)
|
||||
|
||||
for location in world.dynamic_locations:
|
||||
|
@ -292,7 +392,7 @@ def create_playthrough(world):
|
|||
world = copy_world(world)
|
||||
|
||||
# if we only check for beatable, we can do this sanity check first before writing down spheres
|
||||
if world.accessibility == 'none' and not world.can_beat_game():
|
||||
if not world.can_beat_game():
|
||||
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
|
||||
|
||||
# get locations containing progress items
|
||||
|
@ -303,8 +403,7 @@ def create_playthrough(world):
|
|||
sphere_candidates = list(prog_locations)
|
||||
logging.getLogger('').debug('Building up collection spheres.')
|
||||
while sphere_candidates:
|
||||
if not world.keysanity:
|
||||
state.sweep_for_events(key_only=True)
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
sphere = []
|
||||
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
|
@ -323,9 +422,10 @@ def create_playthrough(world):
|
|||
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations))
|
||||
if not sphere:
|
||||
logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
|
||||
if not world.accessibility == 'none':
|
||||
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
|
||||
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.')
|
||||
else:
|
||||
old_world.spoiler.unreachables = sphere_candidates.copy()
|
||||
break
|
||||
|
||||
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
|
||||
|
@ -336,7 +436,6 @@ def create_playthrough(world):
|
|||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
||||
old_item = location.item
|
||||
location.item = None
|
||||
state.remove(old_item)
|
||||
if world.can_beat_game(state_cache[num]):
|
||||
to_delete.append(location)
|
||||
else:
|
||||
|
@ -347,6 +446,14 @@ def create_playthrough(world):
|
|||
for location in to_delete:
|
||||
sphere.remove(location)
|
||||
|
||||
# second phase, sphere 0
|
||||
for item in [i for i in world.precollected_items if i.advancement]:
|
||||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
world.precollected_items.remove(item)
|
||||
world.state.remove(item)
|
||||
if not world.can_beat_game():
|
||||
world.push_precollected(item)
|
||||
|
||||
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
||||
# the previous pruning stage could potentially have made certain items dependant on others
|
||||
# in the same or later sphere (because the location had 2 ways to access but the item originally
|
||||
|
@ -357,8 +464,7 @@ def create_playthrough(world):
|
|||
state = CollectionState(world)
|
||||
collection_spheres = []
|
||||
while required_locations:
|
||||
if not world.keysanity:
|
||||
state.sweep_for_events(key_only=True)
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
sphere = list(filter(state.can_reach, required_locations))
|
||||
|
||||
|
@ -393,10 +499,12 @@ def create_playthrough(world):
|
|||
old_world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
|
||||
for _, path in dict(old_world.spoiler.paths).items():
|
||||
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
||||
if world.mode != 'inverted':
|
||||
if world.mode[player] != 'inverted':
|
||||
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
|
||||
else:
|
||||
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
old_world.spoiler.playthrough = OrderedDict([(str(i + 1), {str(location): str(location.item) for location in sphere}) for i, sphere in enumerate(collection_spheres)])
|
||||
old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
|
||||
for i, sphere in enumerate(collection_spheres):
|
||||
old_world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sphere}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import importlib
|
||||
|
||||
update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled
|
||||
|
||||
|
||||
def update_command():
|
||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade'])
|
||||
|
||||
|
||||
naming_specialties = {"PyYAML": "yaml"} # PyYAML is imported as the name yaml
|
||||
|
||||
|
||||
def update():
|
||||
global update_ran
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt')
|
||||
if not os.path.exists(path):
|
||||
os.path.join(os.path.dirname(__file__), 'requirements.txt')
|
||||
with open(path) as requirementsfile:
|
||||
for line in requirementsfile.readlines():
|
||||
module, remote_version = line.split(">=")
|
||||
module = naming_specialties.get(module, module)
|
||||
try:
|
||||
module = importlib.import_module(module)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input(f'Required python module {module} not found, press enter to install it')
|
||||
update_command()
|
||||
return
|
||||
else:
|
||||
if hasattr(module, "__version__"):
|
||||
module_version = module.__version__
|
||||
module = module.__name__ # also unloads the module to make it writable
|
||||
if type(module_version) == str:
|
||||
module_version = tuple(int(part.strip()) for part in module_version.split("."))
|
||||
remote_version = tuple(int(part.strip()) for part in remote_version.split("."))
|
||||
if module_version < remote_version:
|
||||
input(f'Required python module {module} is outdated ({module_version}<{remote_version}),'
|
||||
' press enter to upgrade it')
|
||||
update_command()
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,182 @@
|
|||
__author__ = "Berserker55" # you can find me on the ALTTP Randomizer Discord
|
||||
|
||||
"""
|
||||
This script launches a Multiplayer "Multiworld" Mystery Game
|
||||
|
||||
.yaml files for all participating players should be placed in a /Players folder.
|
||||
For every player a mystery game is rolled and a ROM created.
|
||||
After generation the server is automatically launched.
|
||||
It is still up to the host to forward the correct port (38281 by default) and distribute the roms to the players.
|
||||
Regular Mystery has to work for this first, such as a ALTTP Base ROM and Enemizer Setup.
|
||||
A guide can be found here: https://docs.google.com/document/d/19FoqUkuyStMqhOq8uGiocskMo1KMjOW4nEeG81xrKoI/edit
|
||||
Configuration can be found in host.yaml
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def feedback(text:str):
|
||||
print(text)
|
||||
input("Press Enter to ignore and probably crash.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(f"{__author__}'s MultiMystery Launcher")
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
|
||||
from Utils import get_public_ipv4, get_options
|
||||
|
||||
from Patch import create_patch_file
|
||||
|
||||
options = get_options()
|
||||
|
||||
multi_mystery_options = options["multi_mystery_options"]
|
||||
output_path = multi_mystery_options["output_path"]
|
||||
enemizer_path = multi_mystery_options["enemizer_path"]
|
||||
player_files_path = multi_mystery_options["player_files_path"]
|
||||
race = multi_mystery_options["race"]
|
||||
create_spoiler = multi_mystery_options["create_spoiler"]
|
||||
zip_roms = multi_mystery_options["zip_roms"]
|
||||
zip_diffs = multi_mystery_options["zip_diffs"]
|
||||
zip_spoiler = multi_mystery_options["zip_spoiler"]
|
||||
zip_multidata = multi_mystery_options["zip_multidata"]
|
||||
zip_format = multi_mystery_options["zip_format"]
|
||||
#zip_password = multi_mystery_options["zip_password"] not at this time
|
||||
player_name = multi_mystery_options["player_name"]
|
||||
meta_file_path = multi_mystery_options["meta_file_path"]
|
||||
teams = multi_mystery_options["teams"]
|
||||
rom_file = options["general_options"]["rom_file"]
|
||||
host = options["server_options"]["host"]
|
||||
port = options["server_options"]["port"]
|
||||
|
||||
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
if not os.path.exists(enemizer_path):
|
||||
feedback(f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
|
||||
if not os.path.exists(rom_file):
|
||||
feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.")
|
||||
player_files = []
|
||||
os.makedirs(player_files_path, exist_ok=True)
|
||||
for file in os.listdir(player_files_path):
|
||||
lfile = file.lower()
|
||||
if lfile.endswith(".yaml") and lfile != meta_file_path.lower():
|
||||
player_files.append(file)
|
||||
print(f"Found player's file {file}.")
|
||||
player_count = len(player_files)
|
||||
if player_count == 0:
|
||||
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
|
||||
else:
|
||||
print(player_count, "Players found.")
|
||||
|
||||
player_string = ""
|
||||
for i, file in enumerate(player_files, 1):
|
||||
player_string += f"--p{i} {os.path.join(player_files_path, file)} "
|
||||
|
||||
|
||||
if os.path.exists("BerserkerMultiServer.exe"):
|
||||
basemysterycommand = "BerserkerMystery.exe" #compiled windows
|
||||
elif os.path.exists("BerserkerMultiServer"):
|
||||
basemysterycommand = "BerserkerMystery" # compiled linux
|
||||
else:
|
||||
basemysterycommand = f"py -{py_version} Mystery.py" #source
|
||||
|
||||
command = f"{basemysterycommand} --multi {len(player_files)} {player_string} " \
|
||||
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
|
||||
f"--outputpath \"{output_path}\" --teams {teams}"
|
||||
|
||||
if create_spoiler:
|
||||
command += " --create_spoiler"
|
||||
if race:
|
||||
command += " --race"
|
||||
if os.path.exists(os.path.join(player_files_path, meta_file_path)):
|
||||
command += f" --meta {os.path.join(player_files_path, meta_file_path)}"
|
||||
|
||||
print(command)
|
||||
import time
|
||||
start = time.perf_counter()
|
||||
text = subprocess.check_output(command, shell=True).decode()
|
||||
print(f"Took {time.perf_counter()-start:.3f} seconds to generate rom(s).")
|
||||
seedname = ""
|
||||
|
||||
for segment in text.split():
|
||||
if segment.startswith("M"):
|
||||
seedname = segment
|
||||
break
|
||||
|
||||
multidataname = f"ER_{seedname}_multidata"
|
||||
spoilername = f"ER_{seedname}_Spoiler.txt"
|
||||
romfilename = ""
|
||||
|
||||
if player_name:
|
||||
for file in os.listdir(output_path):
|
||||
if player_name in file:
|
||||
import webbrowser
|
||||
|
||||
romfilename = os.path.join(output_path, file)
|
||||
print(f"Launching ROM file {romfilename}")
|
||||
webbrowser.open(romfilename)
|
||||
break
|
||||
|
||||
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs)):
|
||||
import zipfile
|
||||
compression = {1 : zipfile.ZIP_DEFLATED,
|
||||
2 : zipfile.ZIP_LZMA,
|
||||
3 : zipfile.ZIP_BZIP2}[zip_format]
|
||||
|
||||
typical_zip_ending = {1: "zip",
|
||||
2: "7z",
|
||||
3: "bz2"}[zip_format]
|
||||
|
||||
def pack_file(file: str):
|
||||
zf.write(os.path.join(output_path, file), file)
|
||||
print(f"Packed {file} into zipfile {zipname}")
|
||||
|
||||
def remove_zipped_file(file: str):
|
||||
os.remove(os.path.join(output_path, file))
|
||||
print(f"Removed {file} which is now present in the zipfile")
|
||||
|
||||
zipname = os.path.join(output_path, f"ER_{seedname}.{typical_zip_ending}")
|
||||
|
||||
print(f"Creating zipfile {zipname}")
|
||||
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
|
||||
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
|
||||
for file in os.listdir(output_path):
|
||||
if file.endswith(".sfc") and seedname in file:
|
||||
if zip_diffs:
|
||||
diff = os.path.split(create_patch_file(os.path.join(output_path, file), ipv4))[1]
|
||||
pack_file(diff)
|
||||
if zip_diffs == 2:
|
||||
remove_zipped_file(diff)
|
||||
if zip_roms:
|
||||
pack_file(file)
|
||||
if zip_roms == 2 and player_name.lower() not in file.lower():
|
||||
remove_zipped_file(file)
|
||||
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
|
||||
pack_file(multidataname)
|
||||
if zip_multidata == 2:
|
||||
remove_zipped_file(multidataname)
|
||||
if zip_spoiler and create_spoiler:
|
||||
pack_file(spoilername)
|
||||
if zip_spoiler == 2:
|
||||
remove_zipped_file(spoilername)
|
||||
|
||||
if os.path.exists(os.path.join(output_path, multidataname)):
|
||||
if os.path.exists("BerserkerMultiServer.exe"):
|
||||
baseservercommand = "BerserkerMultiServer.exe" # compiled windows
|
||||
elif os.path.exists("BerserkerMultiServer"):
|
||||
baseservercommand = "BerserkerMultiServer" # compiled linux
|
||||
else:
|
||||
baseservercommand = f"py -{py_version} MultiServer.py" # source
|
||||
#don't have a mac to test that. If you try to run compiled on mac, good luck.
|
||||
|
||||
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
input("Press enter to close")
|
|
@ -0,0 +1,705 @@
|
|||
import argparse
|
||||
import asyncio
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import zlib
|
||||
import collections
|
||||
import typing
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
import websockets
|
||||
import prompt_toolkit
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from fuzzywuzzy import process as fuzzy_process
|
||||
|
||||
import Items
|
||||
import Regions
|
||||
import Utils
|
||||
from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address
|
||||
|
||||
console_names = frozenset(set(Items.item_table) | set(Regions.location_table))
|
||||
|
||||
|
||||
class Client:
|
||||
version: typing.List[int] = [0, 0, 0]
|
||||
tags: typing.List[str] = []
|
||||
|
||||
def __init__(self, socket: websockets.server.WebSocketServerProtocol):
|
||||
self.socket = socket
|
||||
self.auth = False
|
||||
self.name = None
|
||||
self.team = None
|
||||
self.slot = None
|
||||
self.send_index = 0
|
||||
self.tags = []
|
||||
self.version = [0, 0, 0]
|
||||
|
||||
@property
|
||||
def wants_item_notification(self):
|
||||
return self.auth and "FoundItems" in self.tags
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, host: str, port: int, password: str, location_check_points: int, hint_cost: int,
|
||||
item_cheat: bool):
|
||||
self.data_filename = None
|
||||
self.save_filename = None
|
||||
self.disable_save = False
|
||||
self.player_names = {}
|
||||
self.rom_names = {}
|
||||
self.remote_items = set()
|
||||
self.locations = {}
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.password = password
|
||||
self.server = None
|
||||
self.countdown_timer = 0
|
||||
self.clients = []
|
||||
self.received_items = {}
|
||||
self.location_checks = collections.defaultdict(set)
|
||||
self.hint_cost = hint_cost
|
||||
self.location_check_points = location_check_points
|
||||
self.hints_used = collections.defaultdict(int)
|
||||
self.hints_sent = collections.defaultdict(set)
|
||||
self.item_cheat = item_cheat
|
||||
|
||||
def get_save(self) -> dict:
|
||||
return {
|
||||
"rom_names": list(self.rom_names.items()),
|
||||
"received_items": tuple((k, v) for k, v in self.received_items.items()),
|
||||
"hints_used" : tuple((key,value) for key, value in self.hints_used.items()),
|
||||
"hints_sent" : tuple((key,tuple(value)) for key, value in self.hints_sent.items()),
|
||||
"location_checks" : tuple((key,tuple(value)) for key, value in self.location_checks.items())
|
||||
}
|
||||
|
||||
def set_save(self, savedata: dict):
|
||||
rom_names = savedata["rom_names"]
|
||||
received_items = {tuple(k): [ReceivedItem(*i) for i in v] for k, v in savedata["received_items"]}
|
||||
if not all([self.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
|
||||
raise Exception('Save file mismatch, will start a new game')
|
||||
self.received_items = received_items
|
||||
self.hints_used.update({tuple(key): value for key, value in savedata["hints_used"]})
|
||||
self.hints_sent.update({tuple(key): set(value) for key, value in savedata["hints_sent"]})
|
||||
self.location_checks.update({tuple(key): set(value) for key, value in savedata["location_checks"]})
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in received_items.values()])} received items '
|
||||
f'for {len(received_items)} players')
|
||||
|
||||
|
||||
async def send_msgs(websocket, msgs):
|
||||
if not websocket or not websocket.open or websocket.closed:
|
||||
return
|
||||
try:
|
||||
await websocket.send(json.dumps(msgs))
|
||||
except websockets.ConnectionClosed:
|
||||
pass
|
||||
|
||||
def broadcast_all(ctx : Context, msgs):
|
||||
for client in ctx.clients:
|
||||
if client.auth:
|
||||
asyncio.create_task(send_msgs(client.socket, msgs))
|
||||
|
||||
def broadcast_team(ctx : Context, team, msgs):
|
||||
for client in ctx.clients:
|
||||
if client.auth and client.team == team:
|
||||
asyncio.create_task(send_msgs(client.socket, msgs))
|
||||
|
||||
def notify_all(ctx : Context, text):
|
||||
logging.info("Notice (all): %s" % text)
|
||||
broadcast_all(ctx, [['Print', text]])
|
||||
|
||||
|
||||
def notify_team(ctx: Context, team: int, text: str):
|
||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||
broadcast_team(ctx, team, [['Print', text]])
|
||||
|
||||
|
||||
def notify_client(client: Client, text: str):
|
||||
if not client.auth:
|
||||
return
|
||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
|
||||
|
||||
|
||||
# separated out, due to compatibilty between client's
|
||||
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
|
||||
cmd = [["Hint", hints]]
|
||||
texts = [['Print', format_hint(ctx, team, hint)] for hint in hints]
|
||||
for _, text in texts:
|
||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||
for client in ctx.clients:
|
||||
if client.auth and client.team == team:
|
||||
if "Berserker" in client.tags:
|
||||
payload = cmd
|
||||
else:
|
||||
payload = texts
|
||||
asyncio.create_task(send_msgs(client.socket, payload))
|
||||
|
||||
async def server(websocket, path, ctx: Context):
|
||||
client = Client(websocket)
|
||||
ctx.clients.append(client)
|
||||
|
||||
try:
|
||||
await on_client_connected(ctx, client)
|
||||
async for data in websocket:
|
||||
for msg in json.loads(data):
|
||||
if len(msg) == 1:
|
||||
cmd = msg
|
||||
args = None
|
||||
else:
|
||||
cmd = msg[0]
|
||||
args = msg[1]
|
||||
await process_client_cmd(ctx, client, cmd, args)
|
||||
except Exception as e:
|
||||
if not isinstance(e, websockets.WebSocketException):
|
||||
logging.exception(e)
|
||||
finally:
|
||||
await on_client_disconnected(ctx, client)
|
||||
ctx.clients.remove(client)
|
||||
|
||||
async def on_client_connected(ctx: Context, client: Client):
|
||||
await send_msgs(client.socket, [['RoomInfo', {
|
||||
'password': ctx.password is not None,
|
||||
'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth],
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ['Berserker'],
|
||||
'version': [1, 2, 0]
|
||||
}]])
|
||||
|
||||
async def on_client_disconnected(ctx: Context, client: Client):
|
||||
if client.auth:
|
||||
await on_client_left(ctx, client)
|
||||
|
||||
async def on_client_joined(ctx: Context, client: Client):
|
||||
notify_all(ctx, "%s (Team #%d) has joined the game. Client(%s, %s)." % (client.name, client.team + 1,
|
||||
".".join(str(x) for x in client.version),
|
||||
client.tags))
|
||||
|
||||
async def on_client_left(ctx: Context, client: Client):
|
||||
notify_all(ctx, "%s (Team #%d) has left the game" % (client.name, client.team + 1))
|
||||
|
||||
async def countdown(ctx: Context, timer):
|
||||
notify_all(ctx, f'[Server]: Starting countdown of {timer}s')
|
||||
if ctx.countdown_timer:
|
||||
ctx.countdown_timer = timer # timer is already running, set it to a different time
|
||||
else:
|
||||
ctx.countdown_timer = timer
|
||||
while ctx.countdown_timer > 0:
|
||||
notify_all(ctx, f'[Server]: {ctx.countdown_timer}')
|
||||
ctx.countdown_timer -= 1
|
||||
await asyncio.sleep(1)
|
||||
notify_all(ctx, f'[Server]: GO')
|
||||
|
||||
def get_connected_players_string(ctx: Context):
|
||||
auth_clients = {(c.team, c.slot) for c in ctx.clients if c.auth}
|
||||
|
||||
player_names = sorted(ctx.player_names.keys())
|
||||
current_team = -1
|
||||
text = ''
|
||||
for team, slot in player_names:
|
||||
player_name = ctx.player_names[team, slot]
|
||||
if team != current_team:
|
||||
text += f':: Team #{team + 1}: '
|
||||
current_team = team
|
||||
if (team, slot) in auth_clients:
|
||||
text += f'{player_name} '
|
||||
else:
|
||||
text += f'({player_name}) '
|
||||
return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1]
|
||||
|
||||
|
||||
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[ReceivedItem]:
|
||||
return ctx.received_items.setdefault((team, player), [])
|
||||
|
||||
|
||||
def tuplize_received_items(items):
|
||||
return [(item.item, item.location, item.player) for item in items]
|
||||
|
||||
|
||||
def send_new_items(ctx: Context):
|
||||
for client in ctx.clients:
|
||||
if not client.auth:
|
||||
continue
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if len(items) > client.send_index:
|
||||
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
|
||||
client.send_index = len(items)
|
||||
|
||||
|
||||
def forfeit_player(ctx: Context, team: int, slot: int):
|
||||
all_locations = {values[0] for values in Regions.location_table.values() if type(values[0]) is int}
|
||||
notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||
register_location_checks(ctx, team, slot, all_locations)
|
||||
|
||||
|
||||
def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
||||
found_items = False
|
||||
for location in locations:
|
||||
if (location, slot) in ctx.locations:
|
||||
target_item, target_player = ctx.locations[(location, slot)]
|
||||
if target_player != slot or slot in ctx.remote_items:
|
||||
found = False
|
||||
recvd_items = get_received_items(ctx, team, target_player)
|
||||
for recvd_item in recvd_items:
|
||||
if recvd_item.location == location and recvd_item.player == slot:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
new_item = ReceivedItem(target_item, location, slot)
|
||||
recvd_items.append(new_item)
|
||||
if slot != target_player:
|
||||
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||
found_items = True
|
||||
elif target_player == slot: # local pickup, notify clients of the pickup
|
||||
if location not in ctx.location_checks[team, slot]:
|
||||
for client in ctx.clients:
|
||||
if client.team == team and client.wants_item_notification:
|
||||
asyncio.create_task(
|
||||
send_msgs(client.socket, [['ItemFound', (target_item, location, slot)]]))
|
||||
ctx.location_checks[team, slot] |= set(locations)
|
||||
send_new_items(ctx)
|
||||
|
||||
if found_items:
|
||||
save(ctx)
|
||||
|
||||
|
||||
def save(ctx: Context):
|
||||
if not ctx.disable_save:
|
||||
try:
|
||||
jsonstr = json.dumps(ctx.get_save())
|
||||
with open(ctx.save_filename, "wb") as f:
|
||||
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[Utils.Hint]:
|
||||
hints = []
|
||||
seeked_item_id = Items.item_table[item][3]
|
||||
for check, result in ctx.locations.items():
|
||||
item_id, receiving_player = result
|
||||
if receiving_player == slot and item_id == seeked_item_id:
|
||||
location_id, finding_player = check
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found))
|
||||
|
||||
return hints
|
||||
|
||||
|
||||
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[Utils.Hint]:
|
||||
hints = []
|
||||
seeked_location = Regions.location_table[location][0]
|
||||
for check, result in ctx.locations.items():
|
||||
location_id, finding_player = check
|
||||
if finding_player == slot and location_id == seeked_location:
|
||||
item_id, receiving_player = result
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found))
|
||||
break # each location has 1 item
|
||||
return hints
|
||||
|
||||
|
||||
def format_hint(ctx: Context, team: int, hint: Utils.Hint) -> str:
|
||||
return f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{Items.lookup_id_to_name[hint.item]} can be found " \
|
||||
f"at {get_location_name_from_address(hint.location)} " \
|
||||
f"in {ctx.player_names[team, hint.finding_player]}'s World." \
|
||||
+ (" (found)" if hint.found else "")
|
||||
|
||||
|
||||
def get_intended_text(input_text: str, possible_answers: typing.Iterable[str]= console_names) -> typing.Tuple[str, bool, str]:
|
||||
picks = fuzzy_process.extract(input_text, possible_answers, limit=2)
|
||||
dif = picks[0][1] - picks[1][1]
|
||||
if picks[0][1] == 100:
|
||||
return picks[0][0], True, "Perfect Match"
|
||||
elif picks[0][1] < 75:
|
||||
return picks[0][0], False, f"Didn't find something that closely matches, did you mean {picks[0][0]}?"
|
||||
elif dif > 5:
|
||||
return picks[0][0], True, "Close Match"
|
||||
else:
|
||||
return picks[0][0], False, f"Too many close matches, did you mean {picks[0][0]}?"
|
||||
|
||||
|
||||
async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
||||
if type(cmd) is not str:
|
||||
await send_msgs(client.socket, [['InvalidCmd']])
|
||||
return
|
||||
|
||||
if cmd == 'Connect':
|
||||
if not args or type(args) is not dict or \
|
||||
'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||
'rom' not in args or type(args['rom']) is not list:
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
|
||||
return
|
||||
|
||||
errors = set()
|
||||
if ctx.password is not None and args['password'] != ctx.password:
|
||||
errors.add('InvalidPassword')
|
||||
|
||||
if tuple(args['rom']) not in ctx.rom_names:
|
||||
errors.add('InvalidRom')
|
||||
else:
|
||||
team, slot = ctx.rom_names[tuple(args['rom'])]
|
||||
if any([c.slot == slot and c.team == team for c in ctx.clients if c.auth]):
|
||||
errors.add('SlotAlreadyTaken')
|
||||
else:
|
||||
client.name = ctx.player_names[(team, slot)]
|
||||
client.team = team
|
||||
client.slot = slot
|
||||
|
||||
if errors:
|
||||
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
|
||||
else:
|
||||
client.auth = True
|
||||
client.version = args.get('version', Client.version)
|
||||
client.tags = args.get('tags', Client.tags)
|
||||
reply = [['Connected', [(client.team, client.slot),
|
||||
[(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]]
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
|
||||
client.send_index = len(items)
|
||||
await send_msgs(client.socket, reply)
|
||||
await on_client_joined(ctx, client)
|
||||
|
||||
if not client.auth:
|
||||
return
|
||||
|
||||
if cmd == 'Sync':
|
||||
items = get_received_items(ctx, client.team, client.slot)
|
||||
if items:
|
||||
client.send_index = len(items)
|
||||
await send_msgs(client.socket, [['ReceivedItems', (0, tuplize_received_items(items))]])
|
||||
|
||||
if cmd == 'LocationChecks':
|
||||
if type(args) is not list:
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
|
||||
return
|
||||
register_location_checks(ctx, client.team, client.slot, args)
|
||||
|
||||
if cmd == 'LocationScouts':
|
||||
if type(args) is not list:
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
|
||||
return
|
||||
locs = []
|
||||
for location in args:
|
||||
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
|
||||
return
|
||||
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
||||
|
||||
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
|
||||
item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
|
||||
if item_type:
|
||||
target_item = replacements.get(item_type[0], target_item)
|
||||
|
||||
locs.append([loc_name, location, target_item, target_player])
|
||||
|
||||
logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
||||
await send_msgs(client.socket, [['LocationInfo', [l[1:] for l in locs]]])
|
||||
|
||||
if cmd == 'UpdateTags':
|
||||
if not args or type(args) is not list:
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'UpdateTags']])
|
||||
return
|
||||
client.tags = args
|
||||
|
||||
if cmd == 'Say':
|
||||
if type(args) is not str or not args.isprintable():
|
||||
await send_msgs(client.socket, [['InvalidArguments', 'Say']])
|
||||
return
|
||||
|
||||
notify_all(ctx, client.name + ': ' + args)
|
||||
|
||||
if args.startswith('!players'):
|
||||
notify_all(ctx, get_connected_players_string(ctx))
|
||||
elif args.startswith('!forfeit'):
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
elif args.startswith('!countdown'):
|
||||
try:
|
||||
timer = int(args.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
timer = 10
|
||||
asyncio.create_task(countdown(ctx, timer))
|
||||
elif args.startswith('!getitem') and ctx.item_cheat:
|
||||
item_name = args[9:].lower()
|
||||
item_name, usable, response = get_intended_text(item_name, Items.item_table.keys())
|
||||
if usable:
|
||||
new_item = ReceivedItem(Items.item_table[item_name][3], -1, client.slot)
|
||||
get_received_items(ctx, client.team, client.slot).append(new_item)
|
||||
notify_all(ctx, 'Cheat console: sending "' + item_name + '" to ' + client.name)
|
||||
send_new_items(ctx)
|
||||
else:
|
||||
notify_client(client, response)
|
||||
elif args.startswith("!hint"):
|
||||
points_available = ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - \
|
||||
ctx.hint_cost * ctx.hints_used[client.team, client.slot]
|
||||
item_name = args[6:]
|
||||
|
||||
if not item_name:
|
||||
notify_client(client, "Use !hint {item_name/location_name}, "
|
||||
"for example !hint Lamp or !hint Link's House. "
|
||||
f"A hint costs {ctx.hint_cost} points. "
|
||||
f"You have {points_available} points.")
|
||||
for item_name in ctx.hints_sent[client.team, client.slot]:
|
||||
if item_name in Items.item_table: # item name
|
||||
hints = collect_hints(ctx, client.team, client.slot, item_name)
|
||||
else: # location name
|
||||
hints = collect_hints_location(ctx, client.team, client.slot, item_name)
|
||||
notify_hints(ctx, client.team, hints)
|
||||
else:
|
||||
item_name, usable, response = get_intended_text(item_name)
|
||||
if usable:
|
||||
if item_name in Items.hint_blacklist:
|
||||
notify_client(client, f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif item_name in Items.item_table: # item name
|
||||
hints = collect_hints(ctx, client.team, client.slot, item_name)
|
||||
else: # location name
|
||||
hints = collect_hints_location(ctx, client.team, client.slot, item_name)
|
||||
|
||||
if hints:
|
||||
if item_name in ctx.hints_sent[client.team, client.slot]:
|
||||
notify_hints(ctx, client.team, hints)
|
||||
notify_client(client, "Hint was previously used, no points deducted.")
|
||||
else:
|
||||
found = 0
|
||||
for hint in hints:
|
||||
found += 1 - hint.found
|
||||
if not found:
|
||||
notify_hints(ctx, client.team, hints)
|
||||
notify_client(client, "No new items found, no points deducted.")
|
||||
else:
|
||||
if ctx.hint_cost:
|
||||
can_pay = points_available // (ctx.hint_cost * found) >= 1
|
||||
else:
|
||||
can_pay = True
|
||||
|
||||
if can_pay:
|
||||
ctx.hints_used[client.team, client.slot] += found
|
||||
ctx.hints_sent[client.team, client.slot].add(item_name)
|
||||
notify_hints(ctx, client.team, hints)
|
||||
save(ctx)
|
||||
else:
|
||||
notify_client(client, f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least {ctx.hint_cost}, "
|
||||
f"more if multiple items are still to be found.")
|
||||
else:
|
||||
notify_client(client, "Nothing found. Item/Location may not exist.")
|
||||
else:
|
||||
notify_client(client, response)
|
||||
|
||||
|
||||
def set_password(ctx : Context, password):
|
||||
ctx.password = password
|
||||
logging.warning('Password set to ' + password if password else 'Password disabled')
|
||||
|
||||
|
||||
async def console(ctx: Context):
|
||||
session = prompt_toolkit.PromptSession()
|
||||
running = True
|
||||
while running:
|
||||
with patch_stdout():
|
||||
input_text = await session.prompt_async()
|
||||
try:
|
||||
|
||||
command = input_text.split()
|
||||
if not command:
|
||||
continue
|
||||
|
||||
if command[0] == '/exit':
|
||||
await ctx.server.ws_server._close()
|
||||
running = False
|
||||
|
||||
if command[0] == '/players':
|
||||
logging.info(get_connected_players_string(ctx))
|
||||
if command[0] == '/password':
|
||||
set_password(ctx, command[1] if len(command) > 1 else None)
|
||||
if command[0] == '/kick' and len(command) > 1:
|
||||
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
|
||||
for client in ctx.clients:
|
||||
if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team):
|
||||
if client.socket and not client.socket.closed:
|
||||
await client.socket.close()
|
||||
|
||||
if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit():
|
||||
if len(command) > 2 and command[2].isdigit():
|
||||
team = int(command[1]) - 1
|
||||
slot = int(command[2])
|
||||
else:
|
||||
team = 0
|
||||
slot = int(command[1])
|
||||
forfeit_player(ctx, team, slot)
|
||||
if command[0] == '/forfeitplayer' and len(command) > 1:
|
||||
seeked_player = command[1].lower()
|
||||
for (team, slot), name in ctx.player_names.items():
|
||||
if name.lower() == seeked_player:
|
||||
forfeit_player(ctx, team, slot)
|
||||
if command[0] == '/senditem':
|
||||
if len(command) <= 2:
|
||||
logging.info("Use /senditem {Playername} {itemname}\nFor example /senditem Berserker Lamp")
|
||||
else:
|
||||
seeked_player, usable, response = get_intended_text(command[1], ctx.player_names.values())
|
||||
if usable:
|
||||
item = " ".join(command[2:])
|
||||
item, usable, response = get_intended_text(item, Items.item_table.keys())
|
||||
if usable:
|
||||
for client in ctx.clients:
|
||||
if client.name == seeked_player:
|
||||
new_item = ReceivedItem(Items.item_table[item][3], -1, client.slot)
|
||||
get_received_items(ctx, client.team, client.slot).append(new_item)
|
||||
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
|
||||
send_new_items(ctx)
|
||||
else:
|
||||
logging.warning(response)
|
||||
else:
|
||||
logging.warning(response)
|
||||
if command[0] == '/hint':
|
||||
if len(command) <= 2:
|
||||
logging.info("Use /hint {Playername} {itemname/locationname}\nFor example /hint Berserker Lamp")
|
||||
else:
|
||||
seeked_player, usable, response = get_intended_text(command[1], ctx.player_names.values())
|
||||
if usable:
|
||||
for (team, slot), name in ctx.player_names.items():
|
||||
if name == seeked_player:
|
||||
item = " ".join(command[2:])
|
||||
item, usable, response = get_intended_text(item)
|
||||
if usable:
|
||||
if item in Items.item_table: #item name
|
||||
hints = collect_hints(ctx, team, slot, item)
|
||||
notify_hints(ctx, team, hints)
|
||||
else: #location name
|
||||
hints = collect_hints_location(ctx, team, slot, item)
|
||||
notify_hints(ctx, team, hints)
|
||||
else:
|
||||
logging.warning(response)
|
||||
else:
|
||||
logging.warning(response)
|
||||
|
||||
if command[0][0] != '/':
|
||||
notify_all(ctx, '[Server]: ' + input_text)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
async def forward_port(port: int):
|
||||
import upnpy
|
||||
import socket
|
||||
|
||||
upnp = upnpy.UPnP()
|
||||
upnp.discover()
|
||||
device = upnp.get_igd()
|
||||
|
||||
service = device['WANPPPConnection.1']
|
||||
|
||||
# get own lan IP
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
|
||||
# This specific action returns an empty dict: {}
|
||||
service.AddPortMapping(
|
||||
NewRemoteHost='',
|
||||
NewExternalPort=port,
|
||||
NewProtocol='TCP',
|
||||
NewInternalPort=port,
|
||||
NewInternalClient=ip,
|
||||
NewEnabled=1,
|
||||
NewPortMappingDescription='Berserker\'s Multiworld',
|
||||
NewLeaseDuration=60 * 60 * 24 # 24 hours
|
||||
)
|
||||
|
||||
logging.info(f"Attempted to forward port {port} to {ip}, your local ip address.")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
defaults = Utils.get_options()["server_options"]
|
||||
parser.add_argument('--host', default=defaults["host"])
|
||||
parser.add_argument('--port', default=defaults["port"], type=int)
|
||||
parser.add_argument('--password', default=defaults["password"])
|
||||
parser.add_argument('--multidata', default=defaults["multidata"])
|
||||
parser.add_argument('--savefile', default=defaults["savefile"])
|
||||
parser.add_argument('--disable_save', default=defaults["disable_save"], action='store_true')
|
||||
parser.add_argument('--loglevel', default=defaults["loglevel"],
|
||||
choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
|
||||
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
|
||||
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
|
||||
parser.add_argument('--port_forward', default=defaults["port_forward"], action='store_true')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
portforwardtask = None
|
||||
if args.port_forward:
|
||||
portforwardtask = asyncio.create_task(forward_port(args.port))
|
||||
|
||||
ctx = Context(args.host, args.port, args.password, args.location_check_points, args.hint_cost,
|
||||
not args.disable_item_cheat)
|
||||
|
||||
ctx.data_filename = args.multidata
|
||||
|
||||
try:
|
||||
if not ctx.data_filename:
|
||||
import tkinter
|
||||
import tkinter.filedialog
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
ctx.data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data","*multidata"),))
|
||||
|
||||
with open(ctx.data_filename, 'rb') as f:
|
||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||
for team, names in enumerate(jsonobj['names']):
|
||||
for player, name in enumerate(names, 1):
|
||||
ctx.player_names[(team, player)] = name
|
||||
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']}
|
||||
ctx.remote_items = set(jsonobj['remote_items'])
|
||||
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']}
|
||||
except Exception as e:
|
||||
logging.error('Failed to read multiworld data (%s)' % e)
|
||||
return
|
||||
|
||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||
|
||||
|
||||
|
||||
ctx.disable_save = args.disable_save
|
||||
if not ctx.disable_save:
|
||||
if not ctx.save_filename:
|
||||
ctx.save_filename = (ctx.data_filename[:-9] if ctx.data_filename[-9:] == 'multidata' else (
|
||||
ctx.data_filename + '_')) + 'multisave'
|
||||
try:
|
||||
with open(ctx.save_filename, 'rb') as f:
|
||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||
ctx.set_save(jsonobj)
|
||||
except FileNotFoundError:
|
||||
logging.error('No save data found, starting a new game')
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
if portforwardtask:
|
||||
try:
|
||||
await portforwardtask
|
||||
except:
|
||||
logging.exception("Automatic port forwarding failed with:")
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||
'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||
await ctx.server
|
||||
await console(ctx)
|
||||
|
||||
if __name__ == '__main__':
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main(parse_args()))
|
||||
loop.close()
|
|
@ -0,0 +1,304 @@
|
|||
import argparse
|
||||
import logging
|
||||
import random
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import typing
|
||||
import os
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import parse_yaml
|
||||
from Rom import get_sprite_from_name
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||
multiargs, _ = parser.parse_known_args()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--weights',
|
||||
help='Path to the weights file to use for rolling game settings, urls are also valid')
|
||||
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
|
||||
action='store_true')
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--create_spoiler', action='store_true')
|
||||
parser.add_argument('--rom')
|
||||
parser.add_argument('--enemizercli')
|
||||
parser.add_argument('--outputpath')
|
||||
parser.add_argument('--race', action='store_true')
|
||||
parser.add_argument('--meta', default=None)
|
||||
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.seed is None:
|
||||
random.seed(None)
|
||||
seed = random.randint(0, 999999999)
|
||||
else:
|
||||
seed = args.seed
|
||||
random.seed(seed)
|
||||
|
||||
seedname = "M"+(f"{random.randint(0, 999999999)}".zfill(9))
|
||||
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
||||
|
||||
weights_cache = {}
|
||||
if args.weights:
|
||||
try:
|
||||
weights_cache[args.weights] = get_weights(args.weights)
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
|
||||
print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}")
|
||||
if args.meta:
|
||||
try:
|
||||
weights_cache[args.meta] = get_weights(args.meta)
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
|
||||
print(f"Meta: {args.meta} >> {weights_cache[args.meta]['meta_description']}")
|
||||
if args.samesettings:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
|
||||
for player in range(1, args.multi + 1):
|
||||
path = getattr(args, f'p{player}')
|
||||
if path:
|
||||
try:
|
||||
if path not in weights_cache:
|
||||
weights_cache[path] = get_weights(path)
|
||||
print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
|
||||
erargs = parse_arguments(['--multi', str(args.multi)])
|
||||
erargs.seed = seed
|
||||
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
|
||||
erargs.create_spoiler = args.create_spoiler
|
||||
erargs.race = args.race
|
||||
erargs.outputname = seedname
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.teams = args.teams
|
||||
|
||||
# set up logger
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.loglevel]
|
||||
logging.basicConfig(format='%(message)s', level=loglevel)
|
||||
|
||||
if args.rom:
|
||||
erargs.rom = args.rom
|
||||
|
||||
if args.enemizercli:
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
|
||||
player_path_cache = {}
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
|
||||
|
||||
if args.meta:
|
||||
for player, path in player_path_cache.items():
|
||||
weights_cache[path].setdefault("meta_ignore", [])
|
||||
meta_weights = weights_cache[args.meta]
|
||||
for key in meta_weights:
|
||||
option = get_choice(key, meta_weights)
|
||||
if option is not None:
|
||||
for player, path in player_path_cache.items():
|
||||
players_meta = weights_cache[path]["meta_ignore"]
|
||||
if key not in players_meta:
|
||||
weights_cache[path][key] = option
|
||||
elif type(players_meta) == dict and option not in players_meta[key]:
|
||||
weights_cache[path][key] = option
|
||||
|
||||
for player in range(1, args.multi + 1):
|
||||
path = player_path_cache[player]
|
||||
if path:
|
||||
try:
|
||||
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
|
||||
if settings.sprite is not None and not os.path.isfile(settings.sprite) and not get_sprite_from_name(settings.sprite):
|
||||
logging.warning(f"Warning: The chosen sprite, \"{settings.sprite}\", for yaml \"{path}\", does not exist.")
|
||||
for k, v in vars(settings).items():
|
||||
if v is not None:
|
||||
getattr(erargs, k)[player] = v
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
|
||||
else:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
if not erargs.name[player]:
|
||||
erargs.name[player] = os.path.split(path)[-1].split(".")[0]
|
||||
|
||||
erargs.names = ",".join(erargs.name[i] for i in range(1, args.multi + 1))
|
||||
del(erargs.name)
|
||||
|
||||
ERmain(erargs, seed)
|
||||
|
||||
|
||||
def get_weights(path):
|
||||
try:
|
||||
if urllib.parse.urlparse(path).scheme:
|
||||
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
|
||||
else:
|
||||
with open(path, 'rb') as f:
|
||||
yaml = str(f.read(), "utf-8")
|
||||
except Exception as e:
|
||||
print('Failed to read weights (%s)' % e)
|
||||
return
|
||||
|
||||
return parse_yaml(yaml)
|
||||
|
||||
|
||||
def interpret_on_off(value):
|
||||
return {"on": True, "off": False}.get(value, value)
|
||||
|
||||
|
||||
def convert_to_on_off(value):
|
||||
return {True: "on", False: "off"}.get(value, value)
|
||||
|
||||
|
||||
def get_choice(option, root) -> typing.Any:
|
||||
if option not in root:
|
||||
return None
|
||||
if type(root[option]) is not dict:
|
||||
return interpret_on_off(root[option])
|
||||
if not root[option]:
|
||||
return None
|
||||
return interpret_on_off(
|
||||
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
|
||||
|
||||
|
||||
def handle_name(name: str):
|
||||
return name.strip().replace(' ', '_')
|
||||
|
||||
|
||||
def roll_settings(weights):
|
||||
ret = argparse.Namespace()
|
||||
ret.name = get_choice('name', weights)
|
||||
if ret.name:
|
||||
ret.name = handle_name(ret.name)
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
if glitches_required not in ['none', 'no_logic']:
|
||||
logging.warning("Only NMG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
|
||||
|
||||
# item_placement = get_choice('item_placement')
|
||||
# not supported in ER
|
||||
|
||||
dungeon_items = get_choice('dungeon_items', weights)
|
||||
if dungeon_items == 'full' or dungeon_items == True:
|
||||
dungeon_items = 'mcsb'
|
||||
elif dungeon_items == 'standard':
|
||||
dungeon_items = ""
|
||||
elif not dungeon_items:
|
||||
dungeon_items = ""
|
||||
|
||||
ret.mapshuffle = get_choice('map_shuffle', weights) if 'map_shuffle' in weights else 'm' in dungeon_items
|
||||
ret.compassshuffle = get_choice('compass_shuffle', weights) if 'compass_shuffle' in weights else 'c' in dungeon_items
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle', weights) if 'smallkey_shuffle' in weights else 's' in dungeon_items
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights) if 'bigkey_shuffle' in weights else 'b' in dungeon_items
|
||||
|
||||
ret.accessibility = get_choice('accessibility', weights)
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle', weights)
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
goal = get_choice('goals', weights)
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'fast_ganon': 'crystals',
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt'
|
||||
}[goal]
|
||||
ret.openpyramid = goal == 'fast_ganon'
|
||||
|
||||
ret.crystals_gt = get_choice('tower_open', weights)
|
||||
|
||||
ret.crystals_ganon = get_choice('ganon_open', weights)
|
||||
|
||||
ret.mode = get_choice('world_state', weights)
|
||||
if ret.mode == 'retro':
|
||||
ret.mode = 'open'
|
||||
ret.retro = True
|
||||
|
||||
ret.hints = get_choice('hints', weights)
|
||||
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[get_choice('weapons', weights)]
|
||||
|
||||
ret.difficulty = get_choice('item_pool', weights)
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality', weights)
|
||||
|
||||
ret.shufflebosses = {'none': 'none',
|
||||
'simple': 'basic',
|
||||
'full': 'normal',
|
||||
'random': 'chaos'
|
||||
}[get_choice('boss_shuffle', weights)]
|
||||
|
||||
ret.shuffleenemies = {'none': 'none',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[get_choice('enemy_shuffle', weights)]
|
||||
|
||||
ret.enemy_damage = {'default': 'default',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
}[get_choice('enemy_damage', weights)]
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health', weights)
|
||||
|
||||
ret.shufflepots = get_choice('pot_shuffle', weights)
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer', weights)) if 'beemizer' in weights else 0
|
||||
|
||||
ret.timer = {'none': False,
|
||||
None: False,
|
||||
False: False,
|
||||
'timed': 'timed',
|
||||
'timed_ohko': 'timed-ohko',
|
||||
'ohko': 'ohko',
|
||||
'timed_countdown': 'timed-countdown',
|
||||
'display': 'display'}[get_choice('timer', weights)] if 'timer' in weights.keys() else False
|
||||
|
||||
ret.progressive = convert_to_on_off(get_choice('progressive', weights)) if "progressive" in weights else 'on'
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
itemvalue = get_choice(item, inventoryweights)
|
||||
if item.startswith(('Progressive ', 'Small Key ', 'Rupee', 'Piece of Heart', 'Boss Heart Container',
|
||||
'Sanctuary Heart Container', 'Arrow', 'Bombs ', 'Bomb ', 'Bottle')) and isinstance(
|
||||
itemvalue, int):
|
||||
for i in range(int(itemvalue)):
|
||||
startitems.append(item)
|
||||
elif itemvalue:
|
||||
startitems.append(item)
|
||||
if glitches_required in ['no_logic'] and 'Pegasus Boots' not in startitems:
|
||||
startitems.append('Pegasus Boots')
|
||||
ret.startinventory = ','.join(startitems)
|
||||
|
||||
ret.remote_items = get_choice('remote_items', weights) if 'remote_items' in weights else False
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
ret.sprite = get_choice('sprite', romweights)
|
||||
ret.disablemusic = get_choice('disablemusic', romweights)
|
||||
ret.extendedmsu = get_choice('extendedmsu', romweights)
|
||||
ret.quickswap = get_choice('quickswap', romweights)
|
||||
ret.fastmenu = get_choice('menuspeed', romweights)
|
||||
ret.heartcolor = get_choice('heartcolor', romweights)
|
||||
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights))
|
||||
ret.ow_palettes = get_choice('ow_palettes', romweights)
|
||||
ret.uw_palettes = get_choice('uw_palettes', romweights)
|
||||
|
||||
return ret
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,104 @@
|
|||
from __future__ import annotations
|
||||
from enum import IntEnum, auto, Enum
|
||||
|
||||
|
||||
class Toggle(IntEnum):
|
||||
off = 0
|
||||
on = 1
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Toggle:
|
||||
if text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
||||
return Toggle.off
|
||||
else:
|
||||
return Toggle.on
|
||||
|
||||
|
||||
class Choice(IntEnum):
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
for option in cls:
|
||||
if option.name == text.lower():
|
||||
return option
|
||||
raise KeyError(
|
||||
f'Could not find option "{text}" for "{cls.__name__}", known options are {", ".join(f"{option.name}" for option in cls)}')
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
no_glitches = auto()
|
||||
no_logic = auto()
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
ganon = auto()
|
||||
fast_ganon = auto()
|
||||
all_dungeons = auto()
|
||||
pedestal = auto()
|
||||
triforce_hunt = auto()
|
||||
|
||||
|
||||
class Accessibility(Choice):
|
||||
locations = auto()
|
||||
items = auto()
|
||||
beatable = auto()
|
||||
|
||||
|
||||
class Crystals(Enum):
|
||||
# can't use IntEnum since there's also random
|
||||
C0 = 0
|
||||
C1 = 1
|
||||
C2 = 2
|
||||
C3 = 3
|
||||
C4 = 4
|
||||
C5 = 5
|
||||
C6 = 6
|
||||
C7 = 7
|
||||
Random = -1
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str) -> Crystals:
|
||||
for option in Crystals:
|
||||
if str(option.value) == text.lower():
|
||||
return option
|
||||
return Crystals.Random
|
||||
|
||||
|
||||
class WorldState(Choice):
|
||||
standard = auto()
|
||||
open = auto()
|
||||
retro = auto()
|
||||
inverted = auto()
|
||||
|
||||
|
||||
class Bosses(Choice):
|
||||
vanilla = auto()
|
||||
simple = auto()
|
||||
full = auto()
|
||||
chaos = auto()
|
||||
|
||||
|
||||
class Enemies(Choice):
|
||||
vanilla = auto()
|
||||
shuffled = auto()
|
||||
chaos = auto()
|
||||
|
||||
|
||||
mapshuffle = Toggle
|
||||
compassshuffle = Toggle
|
||||
keyshuffle = Toggle
|
||||
bigkeyshuffle = Toggle
|
||||
hints = Toggle
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
test = argparse.Namespace()
|
||||
test.logic = Logic.from_text("no_logic")
|
||||
test.mapshuffle = mapshuffle.from_text("ON")
|
||||
try:
|
||||
test.logic = Logic.from_text("owg")
|
||||
except KeyError as e:
|
||||
print(e)
|
||||
if test.mapshuffle:
|
||||
print("Mapshuffle is on")
|
||||
print(test)
|
|
@ -0,0 +1,76 @@
|
|||
import bsdiff4
|
||||
import yaml
|
||||
import os
|
||||
import lzma
|
||||
import hashlib
|
||||
from typing import Tuple, Optional
|
||||
|
||||
import Utils
|
||||
from Rom import JAP10HASH, read_rom
|
||||
|
||||
base_rom_bytes = None
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = None) -> bytes:
|
||||
global base_rom_bytes
|
||||
if not base_rom_bytes:
|
||||
options = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["general_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def generate_patch(rom: bytes, metadata=None) -> bytes:
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
|
||||
patch = yaml.dump({"meta": metadata,
|
||||
"patch": patch})
|
||||
return patch.encode(encoding="utf-8-sig")
|
||||
|
||||
|
||||
def create_patch_file(rom_file_to_patch: str, server: str = "") -> str:
|
||||
bytes = generate_patch(load_bytes(rom_file_to_patch),
|
||||
{
|
||||
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
target = os.path.splitext(rom_file_to_patch)[0] + ".bmbp"
|
||||
write_lzma(bytes, target)
|
||||
return target
|
||||
|
||||
|
||||
def create_rom_file(patch_file) -> Tuple[dict, str]:
|
||||
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
||||
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
|
||||
target = os.path.splitext(patch_file)[0] + ".sfc"
|
||||
with open(target, "wb") as f:
|
||||
f.write(patched_data)
|
||||
return data["meta"], target
|
||||
|
||||
|
||||
def load_bytes(path: str):
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write_lzma(data: bytes, path: str):
|
||||
with lzma.LZMAFile(path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
ipv4 = Utils.get_public_ipv4()
|
||||
import sys
|
||||
|
||||
for rom in sys.argv:
|
||||
if rom.endswith(".sfc"):
|
||||
print(f"Creating patch for {rom}")
|
||||
result = create_patch_file(rom, ipv4)
|
||||
print(f"Created patch {result}")
|
40
Plando.py
40
Plando.py
|
@ -10,7 +10,7 @@ import sys
|
|||
from BaseClasses import World
|
||||
from Regions import create_regions
|
||||
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
|
||||
from Rom import patch_rom, LocalRom, Sprite, write_string_to_rom
|
||||
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
|
||||
|
@ -23,7 +23,8 @@ def main(args):
|
|||
start_time = time.perf_counter()
|
||||
|
||||
# initialize the world
|
||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, args.disablemusic, False, False, False, None, 'none', False)
|
||||
world = World(1, '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('')
|
||||
|
||||
hasher = hashlib.md5()
|
||||
|
@ -36,7 +37,7 @@ def main(args):
|
|||
|
||||
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
|
||||
|
||||
world.difficulty_requirements = difficulties[world.difficulty]
|
||||
world.difficulty_requirements[1] = difficulties[world.difficulty[1]]
|
||||
|
||||
create_regions(world, 1)
|
||||
create_dungeons(world, 1)
|
||||
|
@ -68,13 +69,10 @@ def main(args):
|
|||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
if args.sprite is not None:
|
||||
sprite = Sprite(args.sprite)
|
||||
else:
|
||||
sprite = None
|
||||
|
||||
rom = LocalRom(args.rom)
|
||||
patch_rom(world, 1, rom, args.heartbeep, args.heartcolor, sprite)
|
||||
patch_rom(world, rom, 1, 1, False)
|
||||
|
||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||
|
||||
for textname, texttype, text in text_patches:
|
||||
if texttype == 'text':
|
||||
|
@ -114,16 +112,16 @@ def fill_world(world, plando, text_patches):
|
|||
tr_medallion = medallionstr.strip()
|
||||
elif line.startswith('!mode'):
|
||||
_, modestr = line.split(':', 1)
|
||||
world.mode = modestr.strip()
|
||||
world.mode = {1: modestr.strip()}
|
||||
elif line.startswith('!logic'):
|
||||
_, logicstr = line.split(':', 1)
|
||||
world.logic = logicstr.strip()
|
||||
world.logic = {1: logicstr.strip()}
|
||||
elif line.startswith('!goal'):
|
||||
_, goalstr = line.split(':', 1)
|
||||
world.goal = goalstr.strip()
|
||||
world.goal = {1: goalstr.strip()}
|
||||
elif line.startswith('!light_cone_sewers'):
|
||||
_, sewerstr = line.split(':', 1)
|
||||
world.sewer_light_cone = sewerstr.strip().lower() == 'true'
|
||||
world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'}
|
||||
elif line.startswith('!light_cone_lw'):
|
||||
_, lwconestr = line.split(':', 1)
|
||||
world.light_world_light_cone = lwconestr.strip().lower() == 'true'
|
||||
|
@ -132,19 +130,19 @@ def fill_world(world, plando, text_patches):
|
|||
world.dark_world_light_cone = dwconestr.strip().lower() == 'true'
|
||||
elif line.startswith('!fix_trock_doors'):
|
||||
_, trdstr = line.split(':', 1)
|
||||
world.fix_trock_doors = trdstr.strip().lower() == 'true'
|
||||
world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'}
|
||||
elif line.startswith('!fix_trock_exit'):
|
||||
_, trfstr = line.split(':', 1)
|
||||
world.fix_trock_exit = trfstr.strip().lower() == 'true'
|
||||
world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'}
|
||||
elif line.startswith('!fix_gtower_exit'):
|
||||
_, gtfstr = line.split(':', 1)
|
||||
world.fix_gtower_exit = gtfstr.strip().lower() == 'true'
|
||||
elif line.startswith('!fix_pod_exit'):
|
||||
_, podestr = line.split(':', 1)
|
||||
world.fix_palaceofdarkness_exit = podestr.strip().lower() == 'true'
|
||||
world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'}
|
||||
elif line.startswith('!fix_skullwoods_exit'):
|
||||
_, swestr = line.split(':', 1)
|
||||
world.fix_skullwoods_exit = swestr.strip().lower() == 'true'
|
||||
world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'}
|
||||
elif line.startswith('!check_beatable_only'):
|
||||
_, chkbtstr = line.split(':', 1)
|
||||
world.check_beatable_only = chkbtstr.strip().lower() == 'true'
|
||||
|
@ -172,7 +170,7 @@ def fill_world(world, plando, text_patches):
|
|||
item = ItemFactory(itemstr.strip(), 1)
|
||||
if item is not None:
|
||||
world.push_item(location, item)
|
||||
if item.key:
|
||||
if item.smallkey or item.bigkey:
|
||||
location.event = True
|
||||
elif '<=>' in line:
|
||||
entrance, exit = line.split('<=>', 1)
|
||||
|
@ -211,6 +209,8 @@ def start():
|
|||
help='Select the rate at which the heart beep sound is played at low health.')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')
|
||||
parser.add_argument('--plando', help='Filled out template to use for setting up the rom.')
|
||||
args = parser.parse_args()
|
||||
|
@ -222,8 +222,8 @@ def start():
|
|||
if not os.path.isfile(args.plando):
|
||||
input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.rom):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.' % args.sprite)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
|
||||
# set up logger
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
Mushroom: Mushroom
|
||||
Bottle Merchant: Bottle
|
||||
Flute Spot: Ocarina
|
||||
Flute Spot: Flute
|
||||
Sunken Treasure: Nothing
|
||||
Purple Chest: Nothing
|
||||
Blind's Hideout - Top: Nothing
|
||||
|
|
549
README.md
549
README.md
|
@ -1,489 +1,60 @@
|
|||
# ALttPEntranceRandomizer
|
||||
|
||||
This is a entrance randomizer for _The Legend of Zelda: A Link to the Past_ for the SNES.
|
||||
See https://alttpr.com/ for more details on the normal randomizer.
|
||||
|
||||
# Installation
|
||||
|
||||
Clone this repository and then run ```EntranceRandomizer.py``` (requires Python 3).
|
||||
|
||||
Alternatively, run ```Gui.py``` for a simple graphical user interface.
|
||||
|
||||
For releases, a Windows standalone executable is available for users without Python 3.
|
||||
|
||||
# Settings
|
||||
|
||||
## Game Mode
|
||||
|
||||
### Standard
|
||||
|
||||
Fixes Hyrule Castle Secret Entrance and Front Door, but may lead to weird rain state issues if you exit through the Hyrule Castle side exits before rescuing Zelda in a full shuffle.
|
||||
|
||||
Gives lightcone in Hyrule Castle Sewers even without the Lamp.
|
||||
|
||||
### Open
|
||||
|
||||
This mode starts with the option to start in your house or the sanctuary, you are free to explore.
|
||||
|
||||
Special notes:
|
||||
|
||||
- Uncle already in sewers and most likely does not have a sword.
|
||||
- Sewers do not get a free light cone.
|
||||
- It may be a while before you find a sword, think of other ways to do damage to enemies. (bombs are a great tool, as well as picking up bushes in over world).
|
||||
|
||||
### Swordless
|
||||
|
||||
This mode removes all swords from the itempool. Otherwise just like open.
|
||||
|
||||
Special notes:
|
||||
|
||||
- The Medallions to open Misery Mire and Turtle Rock can be used without a sword if you stand on the symbol.
|
||||
- The curtains in Skull Woods and Hyrule Castle Tower that normally require a sword to cut have been removed.
|
||||
- Ganon takes damage from the Hammer.
|
||||
- The magic barrier to Hyrule Castle Tower can be broken with a Hammer.
|
||||
- The Hammer can be used to activate the Ether and Bombos tablets.
|
||||
|
||||
### Inverted
|
||||
|
||||
This mode is similar to Open but requires the Moon Pearl in order to not transform into a bunny in the Light World and the Sanctuary spawn point is moved to the Dark Sanctuary
|
||||
|
||||
Special Notes:
|
||||
|
||||
- Link's House is shuffled freely. The Dark Sanctuary is shuffled within West Dark World.
|
||||
- There are a number of overworld changes to account for the inability to mirror from the Light World to the Dark World.
|
||||
- Legacy shuffles are not implemente for this mode.
|
||||
|
||||
## Game Logic
|
||||
This determines the Item Requirements for each location.
|
||||
|
||||
### No Glitches
|
||||
|
||||
The game can be completed without knowing how to perform glitches of any kind.
|
||||
|
||||
### Minor Glitches
|
||||
|
||||
May require Fake Flippers, Bunny Revival.
|
||||
|
||||
### No Logic
|
||||
|
||||
Items are placed without regard for progression or the seed being possible. Major glitches are likely required.
|
||||
|
||||
## Game Goal
|
||||
|
||||
### Ganon
|
||||
|
||||
Standard game completion requiring you to collect the 7 crystals, defeat Agahnim 2 and then beat Ganon.
|
||||
|
||||
### Pedestal
|
||||
|
||||
Places the Triforce at the Master Sword Pedestal. Ganon cannot be damaged.
|
||||
|
||||
### All Dungeons
|
||||
|
||||
Ganon cannot be damaged until all dungeons (including Hyrule Castle Tower and Ganons Tower) are cleared.
|
||||
|
||||
### Triforce Hunt
|
||||
|
||||
Triforce Pieces are added to the item pool, and some number of them being found will trigger game completion. Ganon cannot be damaged.
|
||||
By default 30 Triforce Pieces are placed while 20 are needed to beat the game. Both values can be adjusted with the custom item pool feature.
|
||||
|
||||
### Crystals
|
||||
|
||||
Standard game completion requiring you to collect the 7 crystals and then beat Ganon.
|
||||
|
||||
This is only noticeably different if the the Ganon shuffle option is enabled.
|
||||
|
||||
## Game Difficulty
|
||||
|
||||
### Normal
|
||||
|
||||
This is the default setting that has an item pool most similar to the original
|
||||
The Legend of Zelda: A Link to the Past.
|
||||
|
||||
### Hard
|
||||
|
||||
This setting reduces the availability of a variety of minor helpful items, most notably
|
||||
limiting the player to two bottles, a Tempered Sword, and Blue Mail. Several minor game
|
||||
mechanics are adjusted to increase difficulty, most notably weakening potions and preventing
|
||||
the player from having fairies in bottles.
|
||||
|
||||
### Expert
|
||||
|
||||
This setting is a more extreme version of the Hard setting. Potions are further nerfed, the item
|
||||
pool is less helpful, and the player can find no armor, only a Master Sword, and only a single bottle.
|
||||
|
||||
## Timer Setting
|
||||
|
||||
### None
|
||||
|
||||
Does not invoke a timer.
|
||||
|
||||
### Display
|
||||
|
||||
Displays a timer on-screen but does not alter the item pool.
|
||||
This will prevent the dungeon item count feature in Easy and Keysanity from working.
|
||||
|
||||
### Timed
|
||||
|
||||
Displays a count-up timer on screen that can be reduced with Green Clocks and Blue Clocks or
|
||||
increased with Red Clocks found in chests that will be added to the itempool.
|
||||
|
||||
### Timed-OHKO
|
||||
|
||||
Displays a countdown timer on screen that, when it hits zero, will put the player into a one hit
|
||||
knockout state until more time is added to the clock via some of the Green Clocks that will be added
|
||||
to the itempool.
|
||||
|
||||
### OHKO
|
||||
|
||||
The player will be in a one hit knockout state the entire game. This is the same as Timed-OHKO except
|
||||
without the Clock items and the timer permanently at zero.
|
||||
|
||||
### Timed-countdown
|
||||
|
||||
Displays a countdown timer on screen that can be increased with Green Clocks and Blue Clocks or
|
||||
decreased with Red Clocks found in chests that will be added to the itempool. The goal of this mode
|
||||
is to finish the game without the timer reaching zero, but the game will continue uninterrupted if
|
||||
the player runs out of time.
|
||||
|
||||
## Progressive equipment
|
||||
|
||||
Determines if Sword, Shield, and gloves are progressive (upgrading in sequence) or not.
|
||||
|
||||
### On (Default)
|
||||
|
||||
This setting makes swords, shields, armor, and gloves progressive. The first of any type of equipment found
|
||||
by the player will be the lowest level item, and each subsequent find of a category will upgrade that type of
|
||||
equipment.
|
||||
|
||||
### Off
|
||||
|
||||
This setting makes swords, shields, armor, and gloves non-progressive. All of the items of these types will be
|
||||
randomly placed in chests, and the player could find them in any order and thus instantly receive high level equipment.
|
||||
Downgrades are not possible; finding a lower level piece of equipment than what is already in the player's possession
|
||||
will simply do nothing.
|
||||
|
||||
### Random
|
||||
|
||||
This setting makes swords, shields, armor, and gloves randomly either progressive or not. Each category is independently
|
||||
randomized.
|
||||
|
||||
## Item Distribution Algorithm
|
||||
|
||||
Determines how the items are shuffled.
|
||||
|
||||
### Balanced
|
||||
This is a variation of VT26 that aims to strike a balance between the overworld heavy VT25 and the dungeon heavy VT26 algorithm.
|
||||
It does this by reshuffling the remaining locations after placing dungeon items.
|
||||
|
||||
### VT26
|
||||
Items and locations are shuffled like in VT25, and dungeon items are now placed using the same algorithm. When Ganon is not
|
||||
shuffled it includes a slight deliberate bias against having too many desireable items in Ganon's Tower to help counterbalance
|
||||
the sheer number of chests in that single location.
|
||||
|
||||
### VT25
|
||||
Items and locations are shuffled and placed from the top of the lists. The only thing preventing an item from being placed into a spot
|
||||
is if is absolutely impossible to be there given the previous made placement choices. Leads to very uniform but guaranteed solvable distributions.
|
||||
|
||||
### VT22
|
||||
The ordinary VT v8.22 algorithm. Fixes issues in placement in VT21 by discarding all previously skipped and unfilled locations
|
||||
after 2/3 of the progression items were placed to prevent stale late game locations from soaking up the same items all the time.
|
||||
|
||||
### VT21
|
||||
The ordinary VT v8.21 algorithm. Unbiased placement of items into unlocked locations, placing items that unlock new locations first.
|
||||
May lead to distributions that seem a bit wonky (high likelyhood of ice rod in Turtle Rock, for instance)
|
||||
|
||||
### Flood
|
||||
Pushes out items starting from Link's House and is slightly biased to placing progression items with less restrictions. Use for relatively simple distributions.
|
||||
|
||||
### Freshness
|
||||
Alternative approach to VT22 to improve on VT21 flaws. Locations that are skipped because they are currently unreachable increase in
|
||||
staleness, decreasing the likelihood of receiving a progress item.
|
||||
|
||||
## Entrance Shuffle Algorithm
|
||||
|
||||
Determines how locations are shuffled. In all modes other than Insanity and the similar legacy versions, holes shuffle as a pair with the connecting cave and the front
|
||||
two sections of Skull Woods remain confined to the general Skull Woods area. Link's house is never shuffled as a design decision.
|
||||
|
||||
### Vanilla
|
||||
|
||||
Places entrances in the same locations they were in the original The Legend of Zelda: A Link to the Past.
|
||||
|
||||
### Simple
|
||||
|
||||
Shuffles dungeon entrances between each other and keeps all 4-entrance dungeons confined to one location such that dungeons will one to one swap with each other.
|
||||
Other than on Light World Death Mountain, interiors are shuffled but still connect the same points on the overworld. On Death Mountain, entrances are connected more freely.
|
||||
|
||||
### Restricted
|
||||
|
||||
Uses dungeon shuffling from Simple but freely connects remaining entrances. Caves and dungeons with multiple entrances will be confined to one world.
|
||||
|
||||
### Full
|
||||
|
||||
Mixes cave and dungeon entrances freely. Caves and dungeons with multiple entrances will be confined to one world.
|
||||
|
||||
### Crossed
|
||||
|
||||
Mixes cave and dungeon entrances freely, but now connector caves and dungeons can link Light World and Dark World.
|
||||
|
||||
### Insanity
|
||||
|
||||
Decouples entrances and exits from each other and shuffles them freely. Caves that were single entrance in vanilla still can only exit to the same location from which they were entered.
|
||||
|
||||
### Legacy Variants
|
||||
|
||||
Similar to the base shuffles, but the distinction between single entrance and multi-entrance caves from older versions of the randomizer is maintained.
|
||||
Madness_Legacy is the more similar to the modern Insanity. Insanity_Legacy has fake worlds and guaranteed Moon Pearl and Magic Mirror for a very different experience.
|
||||
|
||||
### Dungeon Variants
|
||||
|
||||
The dungeon variants only mix up dungeons and keep the rest of the overworld vanilla.
|
||||
|
||||
## Heartbeep Sound Rate
|
||||
|
||||
Select frequency of beeps when on low health. Can completely disable them.
|
||||
|
||||
## Heart Color
|
||||
|
||||
Select the color of Link's hearts.
|
||||
|
||||
## Menu Speed
|
||||
|
||||
A setting that lets the player set the rate at which the menu opens and closes.
|
||||
|
||||
## Create Spoiler Log
|
||||
|
||||
Output a Spoiler File.
|
||||
|
||||
## Do not Create Patched Rom
|
||||
|
||||
If set, will not produce a patched rom as output. Useful in conjunction with the spoiler log option to batch
|
||||
generate spoilers for statistical analysis.
|
||||
|
||||
## Enable L/R button quickswapping
|
||||
|
||||
Use to enable quick item swap with L/R buttons. Press L and R together to switch the state of items like the Mushroom/Powder pair.
|
||||
|
||||
## Keysanity
|
||||
|
||||
This setting allows dungeon specific items (Small Key, Big Key, Map, Compass) to be distributed anywhere in the world and not just
|
||||
in their native dungeon. Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that
|
||||
is traditionally a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but
|
||||
the rest of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell
|
||||
for which dungeons contain pendants and crystals; finding a Map for a dungeon will allow the overworld map to display its prize.
|
||||
|
||||
## Retro
|
||||
|
||||
This setting turns all Small Keys into universal Small Keys that can be used in any dungeon and are distributed across the world.
|
||||
The Bow now consumed rupees to shoot; the cost is 10 rupees per Wood Arrow and 50 per Silver Arrow. Shooting Wood Arrows requires
|
||||
the purchase of an arrow item from shops, and to account for this and the dynamic use of keys, both Wood Arrows and Small Keys will
|
||||
be added to several shops around the world. Four "take any" caves are added that allow the player to choose between an extra Heart
|
||||
Container and a Bottle being filled with Blue Potion, and one of the four swords from the item pool is placed into a special cave as
|
||||
well. The five caves that are removed for these will be randomly selected single entrance caves that did not contain any items or any shops.
|
||||
In further concert with the Bow changes, all arrows under pots, in chests, and elsewhere in the seed will be replaced with rupees.
|
||||
|
||||
## Place Dungeon Items
|
||||
|
||||
If not set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
|
||||
This may lead to different amount of itempool items being placed in a dungeon than you are used to.
|
||||
|
||||
## Include Ganon's Tower and Pyramid Hole in Shuffle pool
|
||||
|
||||
If set, Ganon's Tower is included in the dungeon shuffle pool and the Pyramid Hole/Exit pair is included in the Holes shuffle pool. Ganon can not be defeated until the primary goal is fulfilled.
|
||||
This setting removes any bias against Ganon's Tower that some algorithms may have.
|
||||
|
||||
## Include Helpful Hints
|
||||
|
||||
If set, the 15 telepathic tiles and 5 storytellers scattered about Hyrule will give helpful hints about various items and entrances. An exact breakdown of the hint
|
||||
distribution is provided as an included text file.
|
||||
|
||||
## Use Custom Item Pool
|
||||
|
||||
If set, the item pool normally associated with your difficulty setting is replaced by the item pool specified in the custom tab. This feature is only supported when the randomizer is run
|
||||
via the GUI; attempting to set this via the command line does nothing.
|
||||
|
||||
## Seed
|
||||
|
||||
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
|
||||
|
||||
## Count
|
||||
|
||||
Use to batch generate multiple seeds with same settings. If a seed number is provided, it will be used for the first seed, then used to derive the next seed (i.e. generating 10 seeds with the same seed number given will produce the same 10 (different) roms each time).
|
||||
|
||||
# Command Line Options
|
||||
|
||||
```
|
||||
-h, --help
|
||||
```
|
||||
|
||||
Show the help message and exit.
|
||||
|
||||
```
|
||||
--create_spoiler
|
||||
```
|
||||
|
||||
Output a Spoiler File (default: False)
|
||||
|
||||
```
|
||||
--logic [{noglitches,minorglitches,nologic}]
|
||||
```
|
||||
|
||||
Select the game logic (default: noglitches)
|
||||
|
||||
```
|
||||
--mode [{standard,open,swordless,inverted}]
|
||||
```
|
||||
|
||||
Select the game mode. (default: open)
|
||||
|
||||
```
|
||||
--goal [{ganon,pedestal,dungeons,triforcehunt,crystals}]
|
||||
```
|
||||
|
||||
Select the game completion goal. (default: ganon)
|
||||
|
||||
```
|
||||
--difficulty [{normal,hard,expert}]
|
||||
```
|
||||
|
||||
Select the game difficulty. Affects available itempool. (default: normal)
|
||||
|
||||
```
|
||||
--item_functionality [{normal,hard,expert}]
|
||||
```
|
||||
|
||||
Select limits on item functionality to increase difficulty. (default: normal)
|
||||
|
||||
```
|
||||
--timer [{none,display,timed,timed-ohko,ohko,timed-countdown}]
|
||||
```
|
||||
|
||||
Select the timer setting. (default: none)
|
||||
|
||||
```
|
||||
--progressive [{on,off,random}]
|
||||
```
|
||||
|
||||
Select the setting for progressive equipment. (default: on)
|
||||
|
||||
```
|
||||
--algorithm [{freshness,flood,vt21,vt22,vt25,vt26,balanced}]
|
||||
```
|
||||
|
||||
Select item distribution algorithm. (default: balanced)
|
||||
|
||||
```
|
||||
--shuffle [{default,simple,restricted,full,crossed,insanity,restricted_legacy,full_legacy,madness_legacy,insanity_legacy,dungeonsfull,dungeonssimple}]
|
||||
```
|
||||
|
||||
Select entrance shuffle algorithm. (default: full)
|
||||
|
||||
```
|
||||
--rom ROM
|
||||
```
|
||||
|
||||
Path to a Japanese 1.0 A Link to the Past Rom. (default: Zelda no Densetsu - Kamigami no Triforce (Japan).sfc)
|
||||
|
||||
```
|
||||
--loglevel [{error,info,warning,debug}]
|
||||
```
|
||||
|
||||
Select level of logging for output. (default: info)
|
||||
|
||||
```
|
||||
--seed SEED
|
||||
```
|
||||
|
||||
Define seed number to generate. (default: None)
|
||||
|
||||
```
|
||||
--count COUNT
|
||||
```
|
||||
|
||||
Set the count option (default: None)
|
||||
|
||||
```
|
||||
--quickswap
|
||||
```
|
||||
|
||||
Use to enable quick item swap with L/R buttons. (default: False)
|
||||
|
||||
```
|
||||
--fastmenu [{normal,instant,double,triple,quadruple,half}]
|
||||
```
|
||||
|
||||
Alters the rate at which the menu opens and closes. (default: normal)
|
||||
|
||||
|
||||
```
|
||||
--disablemusic
|
||||
```
|
||||
|
||||
Disables game music, resulting in the game sound being just the SFX. (default: False)
|
||||
|
||||
```
|
||||
--keysanity
|
||||
```
|
||||
|
||||
Enable Keysanity (default: False)
|
||||
|
||||
```
|
||||
--retro
|
||||
```
|
||||
|
||||
Enable Retro mode (default: False)
|
||||
|
||||
```
|
||||
--nodungeonitems
|
||||
```
|
||||
|
||||
If set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world.
|
||||
This may lead to different amount of itempool items being placed in a dungeon than you are used to. (default: False)
|
||||
|
||||
```
|
||||
--heartbeep [{normal,half,quarter,off}]
|
||||
```
|
||||
|
||||
Select frequency of beeps when on low health. (default: normal)
|
||||
|
||||
```
|
||||
--heartcolor [{red,blue,green,yellow,random}]
|
||||
```
|
||||
|
||||
Select the color of Link\'s heart meter. (default: red)
|
||||
|
||||
```
|
||||
--sprite SPRITE
|
||||
```
|
||||
|
||||
Use to select a different sprite sheet to use for Link. Path to a binary file of length 0x7000 containing the sprite data stored at address 0x80000 in the rom. (default: None)
|
||||
|
||||
```
|
||||
--accessibility [{items,locations,none}]
|
||||
```
|
||||
|
||||
Sets the item/location accessibility rules. (default: items)
|
||||
|
||||
```
|
||||
--hints
|
||||
```
|
||||
|
||||
Enables helpful hints from storytellers and telepathic tiles (default: False)
|
||||
|
||||
```
|
||||
--no-shuffleganon
|
||||
```
|
||||
|
||||
Disables the "Include Ganon's Tower and Pyramid Hole in Shuffle pool" option. (default: Enabled)
|
||||
|
||||
```
|
||||
--suppress_rom
|
||||
```
|
||||
|
||||
Enables the "Do not Create Patched Rom" option. (default: False)
|
||||
|
||||
```
|
||||
--gui
|
||||
```
|
||||
|
||||
Open the graphical user interface. Preloads selections with set command line parameters.
|
||||
Berserker's Multiworld Utilities for Bonta's Multiworld
|
||||
=======================================================
|
||||
|
||||
This is a complete fork of Bonta's Multiworld V31, which assumes you already know how to setup and use that project. Instructions here are only for the additions.
|
||||
This is a drop-in replacement with everything from Bonta's Multiworld included.
|
||||
You can find a guide here: https://docs.google.com/document/d/1r7qs1-MK7YbFf2d-mEUeTy2wHykIf1ALG9pLtVvUbSw/edit#
|
||||
|
||||
Additions/Changes
|
||||
-----------------
|
||||
|
||||
Project
|
||||
* Available in precompiled form for Windows 64Bit on [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page.
|
||||
* Compatible with Python 3.7 and 3.8. Potentially future versions as well.
|
||||
* Update modules if they are too old, preventing a crash when trying to connect among potential other issues
|
||||
* Autoinstall missing modules
|
||||
* Allow newer versions of modules than specified, as they will *usually* not break compatibility
|
||||
* Support for V31 extendedmsu
|
||||
* Has support for binary patching to allow legal distribution of multiworld rom files
|
||||
* Various performance improvements
|
||||
* Various crash fixes
|
||||
|
||||
MultiMystery.py
|
||||
* Allows you to generate a Multiworld with individual player mystery weights. Since weights can also be set to 100%, this also allows for individual settings for each player in a regular multiworld.
|
||||
Basis is a .yaml file that sets these weights. You can find an [easy.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/easy.yaml) in this project folder to get started.
|
||||
* Additional instructions are at the start of the file. Open with a text editor.
|
||||
* Configuration options in the host.yaml file.
|
||||
|
||||
MultiServer.py
|
||||
* Added a try/except to prevent malformed console commands from crashing the entire server
|
||||
* Supports automatic port-forwarding, can be enabled in host.yaml
|
||||
* improved `!players` command, mentioning how many players are currently connected of how many expected and who's missing
|
||||
* /forfeitplayer Playername now works when the player is not currently connected
|
||||
* various commands, like /senditem and /hint use "fuzzy text matching", no longer requiring you to enter a location, player name or item name perfectly
|
||||
* Added /hint command on the server (use just /hint for help on command)
|
||||
can be used as /hint Playername Itemname
|
||||
All Itemnames can be found in Items.py starting at line 25
|
||||
example:
|
||||
/hint Berserker Progressive Sword
|
||||
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Hype Cave - Top in ahhdurr's World
|
||||
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Blind's Hideout - Far Right in Schulzer's World
|
||||
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Palace of Darkness - Map Chest in Thorus's World
|
||||
Notice (Team #1): [Hint]: Berserker's Progressive Sword can be found in Ganons Tower - Map Chest in Will's World
|
||||
* A player-side hint command "!hint" also exists. It needs to be turned on in the host.yaml and is based on points.
|
||||
|
||||
Mystery.py
|
||||
* Defaults to generating a non-race ROM (Bonta's only makes race ROMs at this time)
|
||||
If a race ROM is desired, pass --create-race as argument to it
|
||||
* When an error is generated due to a broken .yaml file, it now mentions in the error trace which file, line and character is the culprit
|
||||
* Option for progressive items, allowing you to turn them off (see easy.yaml for more info)
|
||||
* Rom-Option for extendedmsu (see easy.yaml for more info)
|
||||
* Option for "timer"
|
||||
* Supports new Meta-Mystery mode. Read [meta.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/meta.yaml) for details.
|
||||
* Added `dungeonssimple` and `dungeonsfull` ER modes
|
||||
|
||||
MultiClient.py
|
||||
* Awaits a Qusb2snes connection when started, latching on when available
|
||||
* Terminal improvements
|
||||
* Running it with a patch file will patch out the multiworld rom and then automatically connect to the host that created the multiworld
|
||||
* Cheating is now controlled by the server and can be disabled through host.yaml
|
||||
|
||||
|
|
571
Regions.py
571
Regions.py
|
@ -293,35 +293,26 @@ def create_regions(world, player):
|
|||
create_dw_region(player, 'Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop'])
|
||||
]
|
||||
|
||||
for region_name, (room_id, shopkeeper, replaceable) in shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop = Shop(region, room_id, ShopType.Shop, shopkeeper, replaceable)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, (item, price) in enumerate(default_shop_contents[region_name]):
|
||||
shop.add_inventory(index, item, price)
|
||||
world.initialize_regions()
|
||||
|
||||
region = world.get_region('Capacity Upgrade', player)
|
||||
shop = Shop(region, 0x0115, ShopType.UpgradeShop, 0x04, True)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
shop.add_inventory(0, 'Bomb Upgrade (+5)', 100, 7)
|
||||
shop.add_inventory(1, 'Arrow Upgrade (+5)', 100, 7)
|
||||
world.intialize_regions()
|
||||
|
||||
def create_lw_region(player, name, locations=None, exits=None):
|
||||
def create_lw_region(player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.LightWorld, 'Light World', locations, exits)
|
||||
|
||||
def create_dw_region(player, name, locations=None, exits=None):
|
||||
|
||||
def create_dw_region(player: int, name: str, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.DarkWorld, 'Dark World', locations, exits)
|
||||
|
||||
def create_cave_region(player, name, hint='Hyrule', locations=None, exits=None):
|
||||
|
||||
def create_cave_region(player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.Cave, hint, locations, exits)
|
||||
|
||||
def create_dungeon_region(player, name, hint='Hyrule', locations=None, exits=None):
|
||||
|
||||
def create_dungeon_region(player: int, name: str, hint: str, locations=None, exits=None):
|
||||
return _create_region(player, name, RegionType.Dungeon, hint, locations, exits)
|
||||
|
||||
def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None):
|
||||
|
||||
def _create_region(player: int, name: str, type: RegionType, hint: str, locations=None, exits=None):
|
||||
ret = Region(name, type, hint, player)
|
||||
if locations is None:
|
||||
locations = []
|
||||
|
@ -331,14 +322,15 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None
|
|||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
for location in locations:
|
||||
address, crystal, hint_text = location_table[location]
|
||||
ret.locations.append(Location(player, location, address, crystal, hint_text, ret))
|
||||
address, player_address, crystal, hint_text = location_table[location]
|
||||
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
|
||||
return ret
|
||||
|
||||
def mark_light_world_regions(world):
|
||||
|
||||
def mark_light_world_regions(world, player: int):
|
||||
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.
|
||||
# That is ok. the bunny logic will check for this case and incorporate special rules.
|
||||
queue = collections.deque(region for region in world.regions if region.type == RegionType.LightWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.LightWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
@ -351,7 +343,7 @@ def mark_light_world_regions(world):
|
|||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
queue = collections.deque(region for region in world.regions if region.type == RegionType.DarkWorld)
|
||||
queue = collections.deque(region for region in world.get_regions(player) if region.type == RegionType.DarkWorld)
|
||||
seen = set(queue)
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
|
@ -364,269 +356,278 @@ def mark_light_world_regions(world):
|
|||
seen.add(exit.connected_region)
|
||||
queue.append(exit.connected_region)
|
||||
|
||||
# (room_id, shopkeeper, replaceable)
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, 0xC1, True),
|
||||
'Red Shield Shop': (0x0110, 0xC1, True),
|
||||
'Dark Lake Hylia Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Lumberjack Shop': (0x010F, 0xC1, True),
|
||||
'Village of Outcasts Shop': (0x010F, 0xC1, True),
|
||||
'Dark World Potion Shop': (0x010F, 0xC1, True),
|
||||
'Light World Death Mountain Shop': (0x00FF, 0xA0, True),
|
||||
'Kakariko Shop': (0x011F, 0xA0, True),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, 0xA0, True),
|
||||
'Potion Shop': (0x0109, 0xFF, False),
|
||||
# Bomb Shop not currently modeled as a shop, due to special nature of items
|
||||
}
|
||||
# region, [item]
|
||||
# slot, item, price, max=0, replacement=None, replacement_price=0
|
||||
# item = (item, price)
|
||||
|
||||
def create_shops(world, player: int):
|
||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory) in shop_table.items():
|
||||
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
|
||||
locked = True
|
||||
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
region = world.get_region(region_name, player)
|
||||
shop = Shop(region, room_id, type, shopkeeper, custom, locked)
|
||||
region.shop = shop
|
||||
world.shops.append(shop)
|
||||
for index, item in enumerate(inventory):
|
||||
shop.add_inventory(index, *item)
|
||||
|
||||
# (type, room_id, shopkeeper, custom, locked, [items])
|
||||
# item = (item, price, max=0, replacement=None, replacement_price=0)
|
||||
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
|
||||
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||
default_shop_contents = {
|
||||
'Cave Shop (Dark Death Mountain)': _basic_shop_defaults,
|
||||
'Red Shield Shop': [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)],
|
||||
'Dark Lake Hylia Shop': _dark_world_shop_defaults,
|
||||
'Dark World Lumberjack Shop': _dark_world_shop_defaults,
|
||||
'Village of Outcasts Shop': _dark_world_shop_defaults,
|
||||
'Dark World Potion Shop': _dark_world_shop_defaults,
|
||||
'Light World Death Mountain Shop': _basic_shop_defaults,
|
||||
'Kakariko Shop': _basic_shop_defaults,
|
||||
'Cave Shop (Lake Hylia)': _basic_shop_defaults,
|
||||
'Potion Shop': [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)],
|
||||
shop_table = {
|
||||
'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
|
||||
'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
|
||||
'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
|
||||
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
|
||||
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
|
||||
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
|
||||
'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
|
||||
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
|
||||
}
|
||||
|
||||
location_table = {'Mushroom': (0x180013, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2eb18, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014a, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, False, 'underwater'),
|
||||
'Purple Chest': (0x33d68, False, 'from a box'),
|
||||
"Blind's Hideout - Top": (0xeb0f, False, 'in a basement'),
|
||||
"Blind's Hideout - Left": (0xeb12, False, 'in a basement'),
|
||||
"Blind's Hideout - Right": (0xeb15, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Left": (0xeb18, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Right": (0xeb1b, False, 'in a basement'),
|
||||
"Link's Uncle": (0x2df45, False, 'with your uncle'),
|
||||
'Secret Passage': (0xe971, False, 'near your uncle'),
|
||||
'King Zora': (0xee1c3, False, 'at a high price'),
|
||||
"Zora's Ledge": (0x180149, False, 'near Zora'),
|
||||
'Waterfall Fairy - Left': (0xe9b0, False, 'near a fairy'),
|
||||
'Waterfall Fairy - Right': (0xe9d1, False, 'near a fairy'),
|
||||
"King's Tomb": (0xe97a, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xe98c, False, 'in the dam'),
|
||||
"Link's House": (0xe9bc, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xe9ce, False, 'in the bar'),
|
||||
'Chicken House': (0xe9e9, False, 'near poultry'),
|
||||
"Aginah's Cave": (0xe9f2, False, 'with Aginah'),
|
||||
"Sahasrahla's Hut - Left": (0xea82, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Middle": (0xea85, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Right": (0xea88, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2f1fc, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xea8e, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xea91, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xea94, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xea97, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xea9a, False, 'in a well'),
|
||||
'Blacksmith': (0x18002a, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, False, 'with the bat'),
|
||||
'Sick Kid': (0x339cf, False, 'with the sick'),
|
||||
'Hobo': (0x33e7d, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xeb42, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xeb45, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xeb48, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xeb4b, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xeb4e, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xeb3f, False, 'alone in a cave'),
|
||||
'Library': (0x180012, False, 'near books'),
|
||||
'Potion Shop': (0x180014, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, False, 'on an island'),
|
||||
'Maze Race': (0x180142, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xe98f, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xe9b6, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xe9cb, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xe9c2, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xe977, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xe97d, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xe9b3, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xe9b9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xe9f5, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289b0, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xe974, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xeb0c, False, 'in Hyrule Castle'),
|
||||
"Hyrule Castle - Zelda's Chest": (0xeb09, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xe96e, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xeb5d, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xeb60, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xeb63, False, 'in the sewers'),
|
||||
'Sanctuary': (0xea79, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xeab5, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xeab2, False, 'in Castle Tower'),
|
||||
'Old Man': (0xf69fa, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xeb2a, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xeb2d, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Right': (0xeb30, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Far Right': (0xeb33, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xeb36, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xeb39, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xeb3c, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xe9bf, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xe9ad, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xe9e6, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xe9fb, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xe9f8, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, False, 'on the pyramid'),
|
||||
'Catfish': (0xee185, False, 'with a catfish'),
|
||||
'Stumpy': (0x330c7, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xeb1e, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xeb21, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xeb24, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xeb27, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xe980, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xe983, False, 'near a fairy'),
|
||||
'Brewery': (0xe9ec, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xe9ef, False, 'alone in a home'),
|
||||
'Chest Game': (0xeda8, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xea73, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xea76, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xea7c, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xea7f, False, 'in a connection'),
|
||||
'Spike Cave': (0xea8b, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xeb51, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xeb54, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xeb5a, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xeb57, False, 'across pits'),
|
||||
'Floating Island': (0x180141, False, 'on an island'),
|
||||
'Mimic Cave': (0xe9c5, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xea9d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xe986, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xe989, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xeaa0, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xeaa6, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xeaa3, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xeaa9, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xeaac, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xeaaf, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, False, 'with Arrghus'),
|
||||
"Thieves' Town - Big Key Chest": (0xea04, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Map Chest": (0xea01, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Compass Chest": (0xea07, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Ambush Chest": (0xea0a, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Attic": (0xea0d, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Big Chest": (0xea10, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Blind's Cell": (0xea13, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Boss": (0x180156, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xe992, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xe99b, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xe998, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xe9a1, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xe9c8, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xe99e, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xe9fe, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xe9d4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xe995, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xe9aa, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xe9e3, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xe9e0, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xe9a4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xe9dd, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xea67, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xea6a, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xea5e, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xea61, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xe9da, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xea64, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xea6d, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xea22, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xea1c, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xea1f, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xea16, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xea25, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xea19, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xea34, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xea28, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xea5b, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xea3d, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xea49, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xea37, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xea3a, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xea52, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xea43, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xea4c, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xea4f, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xea55, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xea58, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xea40, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xea46, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, False, 'with Helmasaur King'),
|
||||
"Ganons Tower - Bob's Torch": (0x180161, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Left': (0xead9, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Right': (0xeadc, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Tile Room': (0xeae2, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xeae5, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xeae8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xeab8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xeabb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Map Chest': (0xead3, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Firesnake Room': (0xead0, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, False, "in Ganon's Tower"),
|
||||
"Ganons Tower - Bob's Chest": (0xeadf, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Chest': (0xead6, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Left': (0xeaf4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Right': (0xeaf7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Chest': (0xeaf1, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Validation Chest': (0xeb06, False, "atop Ganon's Tower"),
|
||||
'Ganon': (None, False, 'from me'),
|
||||
'Agahnim 1': (None, False, 'from Ganon\'s wizardry form'),
|
||||
'Agahnim 2': (None, False, 'from Ganon\'s wizardry form'),
|
||||
'Floodgate': (None, False, None),
|
||||
'Frog': (None, False, None),
|
||||
'Missing Smith': (None, False, None),
|
||||
'Dark Blacksmith Ruins': (None, False, None),
|
||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], True, 'Eastern Palace'),
|
||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], True, 'Desert Palace'),
|
||||
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], True, 'Tower of Hera'),
|
||||
'Palace of Darkness - Prize': ([0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], True, 'Palace of Darkness'),
|
||||
'Swamp Palace - Prize': ([0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], True, 'Swamp Palace'),
|
||||
'Thieves\' Town - Prize': ([0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], True, 'Thieves\' Town'),
|
||||
'Skull Woods - Prize': ([0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': ([0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': ([0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': ([0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], True, 'Turtle Rock')}
|
||||
location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
||||
'Bottle Merchant': (0x2eb18, 0x186339, False, 'with a merchant'),
|
||||
'Flute Spot': (0x18014a, 0x18633d, False, 'underground'),
|
||||
'Sunken Treasure': (0x180145, 0x186354, False, 'underwater'),
|
||||
'Purple Chest': (0x33d68, 0x186359, False, 'from a box'),
|
||||
"Blind's Hideout - Top": (0xeb0f, 0x1862e3, False, 'in a basement'),
|
||||
"Blind's Hideout - Left": (0xeb12, 0x1862e6, False, 'in a basement'),
|
||||
"Blind's Hideout - Right": (0xeb15, 0x1862e9, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Left": (0xeb18, 0x1862ec, False, 'in a basement'),
|
||||
"Blind's Hideout - Far Right": (0xeb1b, 0x1862ef, False, 'in a basement'),
|
||||
"Link's Uncle": (0x2df45, 0x18635f, False, 'with your uncle'),
|
||||
'Secret Passage': (0xe971, 0x186145, False, 'near your uncle'),
|
||||
'King Zora': (0xee1c3, 0x186360, False, 'at a high price'),
|
||||
"Zora's Ledge": (0x180149, 0x186358, False, 'near Zora'),
|
||||
'Waterfall Fairy - Left': (0xe9b0, 0x186184, False, 'near a fairy'),
|
||||
'Waterfall Fairy - Right': (0xe9d1, 0x1861a5, False, 'near a fairy'),
|
||||
"King's Tomb": (0xe97a, 0x18614e, False, 'alone in a cave'),
|
||||
'Floodgate Chest': (0xe98c, 0x186160, False, 'in the dam'),
|
||||
"Link's House": (0xe9bc, 0x186190, False, 'in your home'),
|
||||
'Kakariko Tavern': (0xe9ce, 0x1861a2, False, 'in the bar'),
|
||||
'Chicken House': (0xe9e9, 0x1861bd, False, 'near poultry'),
|
||||
"Aginah's Cave": (0xe9f2, 0x1861c6, False, 'with Aginah'),
|
||||
"Sahasrahla's Hut - Left": (0xea82, 0x186256, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Middle": (0xea85, 0x186259, False, 'near the elder'),
|
||||
"Sahasrahla's Hut - Right": (0xea88, 0x18625c, False, 'near the elder'),
|
||||
'Sahasrahla': (0x2f1fc, 0x186365, False, 'with the elder'),
|
||||
'Kakariko Well - Top': (0xea8e, 0x186262, False, 'in a well'),
|
||||
'Kakariko Well - Left': (0xea91, 0x186265, False, 'in a well'),
|
||||
'Kakariko Well - Middle': (0xea94, 0x186268, False, 'in a well'),
|
||||
'Kakariko Well - Right': (0xea97, 0x18626b, False, 'in a well'),
|
||||
'Kakariko Well - Bottom': (0xea9a, 0x18626e, False, 'in a well'),
|
||||
'Blacksmith': (0x18002a, 0x186366, False, 'with the smith'),
|
||||
'Magic Bat': (0x180015, 0x18635e, False, 'with the bat'),
|
||||
'Sick Kid': (0x339cf, 0x186367, False, 'with the sick'),
|
||||
'Hobo': (0x33e7d, 0x186368, False, 'with the hobo'),
|
||||
'Lost Woods Hideout': (0x180000, 0x186348, False, 'near a thief'),
|
||||
'Lumberjack Tree': (0x180001, 0x186349, False, 'in a hole'),
|
||||
'Cave 45': (0x180003, 0x18634b, False, 'alone in a cave'),
|
||||
'Graveyard Cave': (0x180004, 0x18634c, False, 'alone in a cave'),
|
||||
'Checkerboard Cave': (0x180005, 0x18634d, False, 'alone in a cave'),
|
||||
'Mini Moldorm Cave - Far Left': (0xeb42, 0x186316, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Left': (0xeb45, 0x186319, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Right': (0xeb48, 0x18631c, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Far Right': (0xeb4b, 0x18631f, False, 'near Moldorms'),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x180010, 0x18635a, False, 'near Moldorms'),
|
||||
'Ice Rod Cave': (0xeb4e, 0x186322, False, 'in a frozen cave'),
|
||||
'Bonk Rock Cave': (0xeb3f, 0x186313, False, 'alone in a cave'),
|
||||
'Library': (0x180012, 0x18635c, False, 'near books'),
|
||||
'Potion Shop': (0x180014, 0x18635d, False, 'near potions'),
|
||||
'Lake Hylia Island': (0x180144, 0x186353, False, 'on an island'),
|
||||
'Maze Race': (0x180142, 0x186351, False, 'at the race'),
|
||||
'Desert Ledge': (0x180143, 0x186352, False, 'in the desert'),
|
||||
'Desert Palace - Big Chest': (0xe98f, 0x186163, False, 'in Desert Palace'),
|
||||
'Desert Palace - Torch': (0x180160, 0x186362, False, 'in Desert Palace'),
|
||||
'Desert Palace - Map Chest': (0xe9b6, 0x18618a, False, 'in Desert Palace'),
|
||||
'Desert Palace - Compass Chest': (0xe9cb, 0x18619f, False, 'in Desert Palace'),
|
||||
'Desert Palace - Big Key Chest': (0xe9c2, 0x186196, False, 'in Desert Palace'),
|
||||
'Desert Palace - Boss': (0x180151, 0x18633f, False, 'with Lanmolas'),
|
||||
'Eastern Palace - Compass Chest': (0xe977, 0x18614b, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Chest': (0xe97d, 0x186151, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Cannonball Chest': (0xe9b3, 0x186187, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Big Key Chest': (0xe9b9, 0x18618d, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Map Chest': (0xe9f5, 0x1861c9, False, 'in Eastern Palace'),
|
||||
'Eastern Palace - Boss': (0x180150, 0x18633e, False, 'with the Armos'),
|
||||
'Master Sword Pedestal': (0x289b0, 0x186369, False, 'at the pedestal'),
|
||||
'Hyrule Castle - Boomerang Chest': (0xe974, 0x186148, False, 'in Hyrule Castle'),
|
||||
'Hyrule Castle - Map Chest': (0xeb0c, 0x1862e0, False, 'in Hyrule Castle'),
|
||||
"Hyrule Castle - Zelda's Chest": (0xeb09, 0x1862dd, False, 'in Hyrule Castle'),
|
||||
'Sewers - Dark Cross': (0xe96e, 0x186142, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Left': (0xeb5d, 0x186331, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Middle': (0xeb60, 0x186334, False, 'in the sewers'),
|
||||
'Sewers - Secret Room - Right': (0xeb63, 0x186337, False, 'in the sewers'),
|
||||
'Sanctuary': (0xea79, 0x18624d, False, 'in Sanctuary'),
|
||||
'Castle Tower - Room 03': (0xeab5, 0x186289, False, 'in Castle Tower'),
|
||||
'Castle Tower - Dark Maze': (0xeab2, 0x186286, False, 'in Castle Tower'),
|
||||
'Old Man': (0xf69fa, 0x186364, False, 'with the old man'),
|
||||
'Spectacle Rock Cave': (0x180002, 0x18634a, False, 'alone in a cave'),
|
||||
'Paradox Cave Lower - Far Left': (0xeb2a, 0x1862fe, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Left': (0xeb2d, 0x186301, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Right': (0xeb30, 0x186304, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Far Right': (0xeb33, 0x186307, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Lower - Middle': (0xeb36, 0x18630a, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Left': (0xeb39, 0x18630d, False, 'in a cave with seven chests'),
|
||||
'Paradox Cave Upper - Right': (0xeb3c, 0x186310, False, 'in a cave with seven chests'),
|
||||
'Spiral Cave': (0xe9bf, 0x186193, False, 'in spiral cave'),
|
||||
'Ether Tablet': (0x180016, 0x18633b, False, 'at a monolith'),
|
||||
'Spectacle Rock': (0x180140, 0x18634f, False, 'atop a rock'),
|
||||
'Tower of Hera - Basement Cage': (0x180162, 0x18633a, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Map Chest': (0xe9ad, 0x186181, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Key Chest': (0xe9e6, 0x1861ba, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Compass Chest': (0xe9fb, 0x1861cf, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Big Chest': (0xe9f8, 0x1861cc, False, 'in Tower of Hera'),
|
||||
'Tower of Hera - Boss': (0x180152, 0x186340, False, 'with Moldorm'),
|
||||
'Pyramid': (0x180147, 0x186356, False, 'on the pyramid'),
|
||||
'Catfish': (0xee185, 0x186361, False, 'with a catfish'),
|
||||
'Stumpy': (0x330c7, 0x18636a, False, 'with tree boy'),
|
||||
'Digging Game': (0x180148, 0x186357, False, 'underground'),
|
||||
'Bombos Tablet': (0x180017, 0x18633c, False, 'at a monolith'),
|
||||
'Hype Cave - Top': (0xeb1e, 0x1862f2, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Right': (0xeb21, 0x1862f5, False, 'near a bat-like man'),
|
||||
'Hype Cave - Middle Left': (0xeb24, 0x1862f8, False, 'near a bat-like man'),
|
||||
'Hype Cave - Bottom': (0xeb27, 0x1862fb, False, 'near a bat-like man'),
|
||||
'Hype Cave - Generous Guy': (0x180011, 0x18635b, False, 'with a bat-like man'),
|
||||
'Peg Cave': (0x180006, 0x18634e, False, 'alone in a cave'),
|
||||
'Pyramid Fairy - Left': (0xe980, 0x186154, False, 'near a fairy'),
|
||||
'Pyramid Fairy - Right': (0xe983, 0x186157, False, 'near a fairy'),
|
||||
'Brewery': (0xe9ec, 0x1861c0, False, 'alone in a home'),
|
||||
'C-Shaped House': (0xe9ef, 0x1861c3, False, 'alone in a home'),
|
||||
'Chest Game': (0xeda8, 0x18636b, False, 'as a prize'),
|
||||
'Bumper Cave Ledge': (0x180146, 0x186355, False, 'on a ledge'),
|
||||
'Mire Shed - Left': (0xea73, 0x186247, False, 'near sparks'),
|
||||
'Mire Shed - Right': (0xea76, 0x18624a, False, 'near sparks'),
|
||||
'Superbunny Cave - Top': (0xea7c, 0x186250, False, 'in a connection'),
|
||||
'Superbunny Cave - Bottom': (0xea7f, 0x186253, False, 'in a connection'),
|
||||
'Spike Cave': (0xea8b, 0x18625f, False, 'beyond spikes'),
|
||||
'Hookshot Cave - Top Right': (0xeb51, 0x186325, False, 'across pits'),
|
||||
'Hookshot Cave - Top Left': (0xeb54, 0x186328, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Right': (0xeb5a, 0x18632e, False, 'across pits'),
|
||||
'Hookshot Cave - Bottom Left': (0xeb57, 0x18632b, False, 'across pits'),
|
||||
'Floating Island': (0x180141, 0x186350, False, 'on an island'),
|
||||
'Mimic Cave': (0xe9c5, 0x186199, False, 'in a cave of mimicry'),
|
||||
'Swamp Palace - Entrance': (0xea9d, 0x186271, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Map Chest': (0xe986, 0x18615a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Chest': (0xe989, 0x18615d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Compass Chest': (0xeaa0, 0x186274, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Big Key Chest': (0xeaa6, 0x18627a, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - West Chest': (0xeaa3, 0x186277, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Left': (0xeaa9, 0x18627d, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Flooded Room - Right': (0xeaac, 0x186280, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Waterfall Room': (0xeaaf, 0x186283, False, 'in Swamp Palace'),
|
||||
'Swamp Palace - Boss': (0x180154, 0x186342, False, 'with Arrghus'),
|
||||
"Thieves' Town - Big Key Chest": (0xea04, 0x1861d8, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Map Chest": (0xea01, 0x1861d5, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Compass Chest": (0xea07, 0x1861db, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Ambush Chest": (0xea0a, 0x1861de, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Attic": (0xea0d, 0x1861e1, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Big Chest": (0xea10, 0x1861e4, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Blind's Cell": (0xea13, 0x1861e7, False, "in Thieves' Town"),
|
||||
"Thieves' Town - Boss": (0x180156, 0x186344, False, 'with Blind'),
|
||||
'Skull Woods - Compass Chest': (0xe992, 0x186166, False, 'in Skull Woods'),
|
||||
'Skull Woods - Map Chest': (0xe99b, 0x18616f, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Chest': (0xe998, 0x18616c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pot Prison': (0xe9a1, 0x186175, False, 'in Skull Woods'),
|
||||
'Skull Woods - Pinball Room': (0xe9c8, 0x18619c, False, 'in Skull Woods'),
|
||||
'Skull Woods - Big Key Chest': (0xe99e, 0x186172, False, 'in Skull Woods'),
|
||||
'Skull Woods - Bridge Room': (0xe9fe, 0x1861d2, False, 'near Mothula'),
|
||||
'Skull Woods - Boss': (0x180155, 0x186343, False, 'with Mothula'),
|
||||
'Ice Palace - Compass Chest': (0xe9d4, 0x1861a8, False, 'in Ice Palace'),
|
||||
'Ice Palace - Freezor Chest': (0xe995, 0x186169, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Chest': (0xe9aa, 0x18617e, False, 'in Ice Palace'),
|
||||
'Ice Palace - Iced T Room': (0xe9e3, 0x1861b7, False, 'in Ice Palace'),
|
||||
'Ice Palace - Spike Room': (0xe9e0, 0x1861b4, False, 'in Ice Palace'),
|
||||
'Ice Palace - Big Key Chest': (0xe9a4, 0x186178, False, 'in Ice Palace'),
|
||||
'Ice Palace - Map Chest': (0xe9dd, 0x1861b1, False, 'in Ice Palace'),
|
||||
'Ice Palace - Boss': (0x180157, 0x186345, False, 'with Kholdstare'),
|
||||
'Misery Mire - Big Chest': (0xea67, 0x18623b, False, 'in Misery Mire'),
|
||||
'Misery Mire - Map Chest': (0xea6a, 0x18623e, False, 'in Misery Mire'),
|
||||
'Misery Mire - Main Lobby': (0xea5e, 0x186232, False, 'in Misery Mire'),
|
||||
'Misery Mire - Bridge Chest': (0xea61, 0x186235, False, 'in Misery Mire'),
|
||||
'Misery Mire - Spike Chest': (0xe9da, 0x1861ae, False, 'in Misery Mire'),
|
||||
'Misery Mire - Compass Chest': (0xea64, 0x186238, False, 'in Misery Mire'),
|
||||
'Misery Mire - Big Key Chest': (0xea6d, 0x186241, False, 'in Misery Mire'),
|
||||
'Misery Mire - Boss': (0x180158, 0x186346, False, 'with Vitreous'),
|
||||
'Turtle Rock - Compass Chest': (0xea22, 0x1861f6, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Left': (0xea1c, 0x1861f0, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Roller Room - Right': (0xea1f, 0x1861f3, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Chain Chomps': (0xea16, 0x1861ea, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Key Chest': (0xea25, 0x1861f9, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Big Chest': (0xea19, 0x1861ed, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Crystaroller Room': (0xea34, 0x186208, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xea31, 0x186205, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xea2e, 0x186202, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xea2b, 0x1861ff, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xea28, 0x1861fc, False, 'in Turtle Rock'),
|
||||
'Turtle Rock - Boss': (0x180159, 0x186347, False, 'with Trinexx'),
|
||||
'Palace of Darkness - Shooter Room': (0xea5b, 0x18622f, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Bridge': (0xea3d, 0x186211, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Stalfos Basement': (0xea49, 0x18621d, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Key Chest': (0xea37, 0x18620b, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - The Arena - Ledge': (0xea3a, 0x18620e, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Map Chest': (0xea52, 0x186226, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Compass Chest': (0xea43, 0x186217, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Left': (0xea4c, 0x186220, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Basement - Right': (0xea4f, 0x186223, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Top': (0xea55, 0x186229, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0xea58, 0x18622c, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Big Chest': (0xea40, 0x186214, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Harmless Hellway': (0xea46, 0x18621a, False, 'in Palace of Darkness'),
|
||||
'Palace of Darkness - Boss': (0x180153, 0x186341, False, 'with Helmasaur King'),
|
||||
"Ganons Tower - Bob's Torch": (0x180161, 0x186363, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Left': (0xead9, 0x1862ad, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Hope Room - Right': (0xeadc, 0x1862b0, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Tile Room': (0xeae2, 0x1862b6, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Left': (0xeae5, 0x1862b9, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Top Right': (0xeae8, 0x1862bc, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0xeaeb, 0x1862bf, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0xeaee, 0x1862c2, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Left': (0xeab8, 0x18628c, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Top Right': (0xeabb, 0x18628f, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0xeabe, 0x186292, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0xeac1, 0x186295, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Map Chest': (0xead3, 0x1862a7, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Firesnake Room': (0xead0, 0x1862a4, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0xeac4, 0x186298, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0xeac7, 0x18629b, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0xeaca, 0x18629e, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0xeacd, 0x1862a1, False, "in Ganon's Tower"),
|
||||
"Ganons Tower - Bob's Chest": (0xeadf, 0x1862b3, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Chest': (0xead6, 0x1862aa, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Left': (0xeaf4, 0x1862c8, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Room - Right': (0xeaf7, 0x1862cb, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Big Key Chest': (0xeaf1, 0x1862c5, False, "in Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0xeafd, 0x1862d1, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0xeb00, 0x1862d4, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0xeb03, 0x1862d7, False, "atop Ganon's Tower"),
|
||||
'Ganons Tower - Validation Chest': (0xeb06, 0x1862da, False, "atop Ganon's Tower"),
|
||||
'Ganon': (None, None, False, 'from me'),
|
||||
'Agahnim 1': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Agahnim 2': (None, None, False, 'from Ganon\'s wizardry form'),
|
||||
'Floodgate': (None, None, False, None),
|
||||
'Frog': (None, None, False, None),
|
||||
'Missing Smith': (None, None, False, None),
|
||||
'Dark Blacksmith Ruins': (None, None, False, None),
|
||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
|
||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
|
||||
'Tower of Hera - Prize': (
|
||||
[0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
|
||||
'Palace of Darkness - Prize': (
|
||||
[0x120A1, 0x53F00, 0x53F01, 0x180056, 0x18007D, 0xC702], None, True, 'Palace of Darkness'),
|
||||
'Swamp Palace - Prize': (
|
||||
[0x120A0, 0x53F6C, 0x53F6D, 0x180055, 0x180071, 0xC701], None, True, 'Swamp Palace'),
|
||||
'Thieves\' Town - Prize': (
|
||||
[0x120A6, 0x53F36, 0x53F37, 0x18005B, 0x180077, 0xC707], None, True, 'Thieves\' Town'),
|
||||
'Skull Woods - Prize': (
|
||||
[0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': (
|
||||
[0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': (
|
||||
[0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': (
|
||||
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||
|
||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||
lookup_id_to_name[-1] = "cheat console"
|
||||
|
|
8
Text.py
8
Text.py
|
@ -107,6 +107,7 @@ Triforce_texts = [
|
|||
"You get one\nwish. Choose\nwisely, hero!",
|
||||
"Can you please\nbreak us three\nup? Thanks.",
|
||||
" Pick us up\n before we\n get dizzy!",
|
||||
"\n Honk."
|
||||
]
|
||||
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?']
|
||||
|
@ -366,7 +367,7 @@ class Credits(object):
|
|||
SceneLargeCreditLine(23, "Woodsmen's Hut"),
|
||||
],
|
||||
'grove': [
|
||||
SceneSmallCreditLine(19, 'ocarina boy plays again'),
|
||||
SceneSmallCreditLine(19, 'flute boy plays again'),
|
||||
SceneLargeCreditLine(23, 'Haunted Grove'),
|
||||
],
|
||||
'well': [
|
||||
|
@ -427,7 +428,6 @@ class CreditLine(object):
|
|||
|
||||
@property
|
||||
def x(self):
|
||||
x = 0
|
||||
if self.align == 'left':
|
||||
x = 0
|
||||
elif self.align == 'right':
|
||||
|
@ -1311,7 +1311,7 @@ class TextTable(object):
|
|||
'item_get_bombos',
|
||||
'item_get_quake',
|
||||
'item_get_hammer',
|
||||
'item_get_ocarina',
|
||||
'item_get_flute',
|
||||
'item_get_cane_of_somaria',
|
||||
'item_get_hookshot',
|
||||
'item_get_bombs',
|
||||
|
@ -1552,7 +1552,7 @@ class TextTable(object):
|
|||
text['item_get_bombos'] = CompressedTextMapper.convert("Let's set everything on fire, and melt things!")
|
||||
text['item_get_quake'] = CompressedTextMapper.convert("Time to make the earth shake, rattle, and roll!")
|
||||
text['item_get_hammer'] = CompressedTextMapper.convert("STOP!\n\nHammer Time!") # 66
|
||||
text['item_get_ocarina'] = CompressedTextMapper.convert("Finally! We can play the Song of Time!")
|
||||
text['item_get_flute'] = CompressedTextMapper.convert("Finally! We can play the Song of Time!")
|
||||
text['item_get_cane_of_somaria'] = CompressedTextMapper.convert("Make blocks!\nThrow blocks!\nsplode Blocks!")
|
||||
text['item_get_hookshot'] = CompressedTextMapper.convert("BOING!!!\nBOING!!!\nSay no more…")
|
||||
text['item_get_bombs'] = CompressedTextMapper.convert("BOMBS! Use A to pick 'em up, throw 'em, get hurt!")
|
||||
|
|
98
Utils.py
98
Utils.py
|
@ -1,53 +1,84 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
import functools
|
||||
|
||||
from yaml import load
|
||||
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
|
||||
def int16_as_bytes(value):
|
||||
value = value & 0xFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF]
|
||||
|
||||
|
||||
def int32_as_bytes(value):
|
||||
value = value & 0xFFFFFFFF
|
||||
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
|
||||
|
||||
|
||||
def pc_to_snes(value):
|
||||
return ((value<<1) & 0x7F0000)|(value & 0x7FFF)|0x8000
|
||||
|
||||
def snes_to_pc(value):
|
||||
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
|
||||
|
||||
def parse_player_names(names, players, teams):
|
||||
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
|
||||
ret = []
|
||||
while names or len(ret) < teams:
|
||||
team = [n[:16] for n in names[:players]]
|
||||
# where does the 16 character limit come from?
|
||||
while len(team) != players:
|
||||
team.append(f"Player {len(team) + 1}")
|
||||
ret.append(team)
|
||||
|
||||
names = names[players:]
|
||||
return ret
|
||||
|
||||
def is_bundled():
|
||||
return getattr(sys, 'frozen', False)
|
||||
|
||||
def local_path(path):
|
||||
if local_path.cached_path is not None:
|
||||
if local_path.cached_path:
|
||||
return os.path.join(local_path.cached_path, path)
|
||||
|
||||
if is_bundled():
|
||||
# we are running in a bundle
|
||||
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
|
||||
elif is_bundled():
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
# we are running in a PyInstaller bundle
|
||||
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
|
||||
else:
|
||||
# cx_Freeze
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||
else:
|
||||
# we are running in a normal Python environment
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(__file__))
|
||||
import __main__
|
||||
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
||||
|
||||
return os.path.join(local_path.cached_path, path)
|
||||
|
||||
local_path.cached_path = None
|
||||
|
||||
def output_path(path):
|
||||
if output_path.cached_path is not None:
|
||||
if output_path.cached_path:
|
||||
return os.path.join(output_path.cached_path, path)
|
||||
|
||||
if not is_bundled():
|
||||
if not is_bundled() and not hasattr(sys, "_MEIPASS"):
|
||||
# this should trigger if it's cx_freeze bundling
|
||||
output_path.cached_path = '.'
|
||||
return os.path.join(output_path.cached_path, path)
|
||||
else:
|
||||
# has been packaged, so cannot use CWD for output.
|
||||
# has been PyInstaller packaged, so cannot use CWD for output.
|
||||
if sys.platform == 'win32':
|
||||
#windows
|
||||
# windows
|
||||
import ctypes.wintypes
|
||||
CSIDL_PERSONAL = 5 # My Documents
|
||||
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
|
||||
CSIDL_PERSONAL = 5 # My Documents
|
||||
SHGFP_TYPE_CURRENT = 0 # Get current, not default value
|
||||
|
||||
buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
|
||||
ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf)
|
||||
|
@ -96,7 +127,7 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
|||
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))))
|
||||
old_rom_data.extend(bytearray([0x00]) * (2097152 - len(old_rom_data)))
|
||||
|
||||
out_data = OrderedDict()
|
||||
for idx, old in enumerate(old_rom_data):
|
||||
|
@ -107,8 +138,49 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
|||
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=(",", ":"))
|
||||
json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":"))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(new_rom_data)
|
||||
return "New Rom Hash: " + basemd5.hexdigest()
|
||||
|
||||
|
||||
parse_yaml = functools.partial(load, Loader=Loader)
|
||||
|
||||
|
||||
class Hint(typing.NamedTuple):
|
||||
receiving_player: int
|
||||
finding_player: int
|
||||
location: int
|
||||
item: int
|
||||
found: bool
|
||||
|
||||
def get_public_ipv4() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
import logging
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
|
||||
except Exception as e:
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
|
||||
except:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
return ip
|
||||
|
||||
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
locations = ("options.yaml", "host.yaml",
|
||||
local_path("options.yaml"), local_path("host.yaml"))
|
||||
|
||||
for location in locations:
|
||||
if os.path.exists(location):
|
||||
with open(location) as f:
|
||||
get_options.options = parse_yaml(f.read())
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(f"Could not find {locations[1]} to load options.")
|
||||
return get_options.options
|
||||
|
|
|
@ -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
|
43
appveyor.yml
43
appveyor.yml
|
@ -2,27 +2,28 @@ version: '{build}'
|
|||
pull_requests:
|
||||
do_not_increment_build_number: true
|
||||
environment:
|
||||
ProjectVersion: build$(APPVEYOR_BUILD_VERSION)
|
||||
matrix:
|
||||
- PYTHON: C:\PYTHON36
|
||||
- PYTHON: "C:\\Python37-x64\\python.exe"
|
||||
PYTHON_VERSION: "3.7"
|
||||
|
||||
- PYTHON: "C:\\Python38-x64\\python.exe"
|
||||
PYTHON_VERSION: "3.8"
|
||||
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
|
||||
- cmd: "%PYTHON% -m pip install --upgrade pip"
|
||||
- cmd: "%PYTHON% -m pip install -r requirements.txt --upgrade"
|
||||
- cmd: "%PYTHON% -m pip install --upgrade cx_Freeze"
|
||||
build_script:
|
||||
- cmd: "%PYTHON% setup.py build_exe"
|
||||
artifacts:
|
||||
- path: dist/EntranceRandomizer*.msi
|
||||
name: EntranceRandomizer-Installer-$(ProjectVersion)-win32.msi
|
||||
- path: dist/EntranceRandomizer/
|
||||
name: EntranceRandomizer-Raw-$(ProjectVersion)-win32.zip
|
||||
- path: "build\\exe.win-amd64-%PYTHON_VERSION%\\"
|
||||
name: "Berserker_Multiworld_%APPVEYOR_REPO_BRANCH%_Python%PYTHON_VERSION%-x64"
|
||||
deploy:
|
||||
description: 'Appveyor automated build'
|
||||
provider: GitHub
|
||||
auth_token:
|
||||
secure: +cRWefLphFutZuzCcCsNS0tl7nNj/IpnJmfht6hZFh2z9eQdFgcu6zwGS3lWItat
|
||||
artifact: /.*\.zip/
|
||||
draft: false
|
||||
prerelease: false
|
||||
on:
|
||||
APPVEYOR_REPO_TAG: true # deploy on tag push only
|
|
@ -1,39 +0,0 @@
|
|||
# -*- mode: python -*-
|
||||
from PyInstaller.compat import is_win
|
||||
block_cipher = None
|
||||
|
||||
# Todo: the runtime hooks should only be installed on windows
|
||||
a = Analysis(['../EntranceRandomizer.py'],
|
||||
pathex=['bundle'],
|
||||
binaries=[],
|
||||
datas=[('../data/', 'data/'), ('../README.html', '.')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=['bundle/_rt_hook.py'],
|
||||
excludes=['lzma', 'bz2'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=True,
|
||||
name='EntranceRandomizer',
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
icon='../data/ER.ico',
|
||||
console=is_win )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=False,
|
||||
name='EntranceRandomizer')
|
||||
app = BUNDLE(coll,
|
||||
name ='EntranceRandomizer.app',
|
||||
icon = '../data/ER.icns',
|
||||
bundle_identifier = None)
|
|
@ -1,4 +0,0 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
sys.path.append(os.path.join(sys._MEIPASS, "ext"))
|
|
@ -1,35 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="1.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
|
||||
exclude-result-prefixes="msxsl"
|
||||
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi">
|
||||
|
||||
<xsl:output method="xml" indent="no"/>
|
||||
|
||||
<xsl:strip-space elements="*"/>
|
||||
|
||||
<xsl:template match="@*|node()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="@*|node()"/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="wix:File[@Source='SourceDir\EntranceRandomizer.exe']">
|
||||
<xsl:copy-of select="." />
|
||||
<wix:Shortcut Id="ProgramShortcut"
|
||||
Name="ALttP Entrance Randomizer"
|
||||
Advertise="yes"
|
||||
Description="ALttP Entrance Randomizer"
|
||||
Directory="ApplicationProgramsFolder" />
|
||||
</xsl:template>
|
||||
<xsl:template match="wix:File[@Source='SourceDir\README.hmtl']">
|
||||
<xsl:copy-of select="." />
|
||||
<wix:Shortcut Id="ReadmeShortcut"
|
||||
Name="ALttP Entrance Randomizer README"
|
||||
Advertise="yes"
|
||||
Description="ALttP Entrance Randomizer README"
|
||||
Directory="ApplicationProgramsFolder" />
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<!-- Setting the product ID to "*" means all upgrades are treated as major upgrades, and will uninstall the old version
|
||||
before installing the new one. This is desireable because it means nothing breaks if we fail to follow the component rules precisely. -->
|
||||
<Product Id="*" Name="ALttP Entrance Randomizer" Language="1033" Version="$(env.ER_Version)" Manufacturer="Randomizer Community" UpgradeCode="0229C621-5F8A-4D59-962A-5826C58B93DD" >
|
||||
<Package Id="*" InstallerVersion="400" Compressed="yes" InstallScope="perMachine" />
|
||||
<!-- Allowing downgrades will cause a harmless warning to be emitted. This wearning is not relevant here, allowing downgrades via a new major upgrade is safe for a simple standalone app like this-->
|
||||
<MajorUpgrade AllowDowngrades="yes"/>
|
||||
<Media Id="1" Cabinet="contents.cab" EmbedCab="yes" CompressionLevel="high"/>
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id='ProgramFilesFolder' Name='PFiles'>
|
||||
<Directory Id='INSTALLDIR' Name='ALttP Entrance Randomizer'/>
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="ALttP Entrance Randomizer"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="ApplicationShortcut" Guid="0054698A-5A56-4B36-8176-8FEC1762EF2D">
|
||||
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall"/>
|
||||
<RegistryValue Root="HKCU" Key="Software\ALttPEntranceRandomizer" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
<Feature Id="Complete"
|
||||
Title="ALttP Entrance Randomizer"
|
||||
Description="ALttP Entrance Randomizer"
|
||||
Level="1">
|
||||
<ComponentGroupRef Id="ERFiles"/>
|
||||
<ComponentRef Id="ApplicationShortcut"/>
|
||||
</Feature>
|
||||
|
||||
<Icon Id="ER.ico" SourceFile="Data/ER.ico" />
|
||||
<Property Id="DISABLEADVTSHORTCUTS" Secure="yes">1</Property>
|
||||
<Property Id="ARPPRODUCTICON" Value="ER.ico" />
|
||||
<Property Id="WIXUI_INSTALLDIR">INSTALLDIR</Property>
|
||||
<UI>
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
<UIRef Id="WixUI_ErrorProgressText" />
|
||||
<!-- Skip license page -->
|
||||
<Publish Dialog="WelcomeDlg"
|
||||
Control="Next"
|
||||
Event="NewDialog"
|
||||
Value="InstallDirDlg"
|
||||
Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg"
|
||||
Control="Back"
|
||||
Event="NewDialog"
|
||||
Value="WelcomeDlg"
|
||||
Order="2">1</Publish>
|
||||
</UI>
|
||||
</Product>
|
||||
</Wix>
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,160 @@
|
|||
#More general info here: https://docs.google.com/document/d/1r7qs1-MK7YbFf2d-mEUeTy2wHykIf1ALG9pLtVvUbSw/edit
|
||||
description: Easy/Open/Normal #please describe your options. Especially useful when you have multiple yamls for different occasions
|
||||
name: PleaseEnterNameHere #your name ingame, space and "_" gets replaced with a dash "-"
|
||||
glitches_required: none #there is also no_logic.
|
||||
item_placement: basic #this is based on Entrance Randomizer, which does not (yet?) support advanced
|
||||
map_shuffle: #to shuffle dungeon maps into the outside world and other dungeons, as well as other player's worlds in multiworld
|
||||
on: 0
|
||||
off: 1
|
||||
compass_shuffle: #same for compass
|
||||
on: 0
|
||||
off: 1
|
||||
smallkey_shuffle: #same for small keys
|
||||
on: 0
|
||||
off: 1
|
||||
bigkey_shuffle: #same for big keys
|
||||
on: 0
|
||||
off: 1
|
||||
dungeon_items: # alternative to the 4 shuffles above this, does nothing until the respective 4 shuffles are deleted
|
||||
mc: 0 # shuffle Maps and Compass
|
||||
none: 1 # shuffle none of the 4
|
||||
mcsb: 0 # shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
|
||||
accessibility:
|
||||
items: 0 # item accessibility means you can get all inventory items. So a key could lock itself, but you can fill your inventory
|
||||
locations: 1 # location accessibility means you can access every location in your seed and get all 216 checks
|
||||
none: 0 # no accessibility means your seed is "beatable only", meaning any items you do not need to beat the game can be unreachable. This can mean you have to defeat ganon with a lamp and master sword.
|
||||
progressive: #not available in bonta's multiworld at this time. If you want this option, make sure the host uses the correct Multiworld
|
||||
on: 1 # progressive items, you will always get progressive items like swords in their order: figher sword -> master sword -> tempered sword -> golden sword
|
||||
off: 0 # turns progressive items off, so you can find, for example, silver arrows before a bow
|
||||
random: 0 # rolls a 50/50 chance for each potentially progressive item. So, for example, you can have progressive swords but non-progressive mittens
|
||||
entrance_shuffle:
|
||||
none: 1 # no entrance shuffle
|
||||
dungeonssimple: 0 # shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
|
||||
dungeonsfull: 0 # shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons
|
||||
simple: 0 #dungeons are shuffled with each other and the other entrances are shuffled with each other
|
||||
restricted: 0 #dungeons still shuffle along each other but connects other entrances more feely with each other while keeping entrances in one world
|
||||
full: 0 # mixes caves and dungeons freely, except for confining all entrances to one world
|
||||
crossed: 0 #introduces cross world connectors
|
||||
insanity: 0 #any entrance can lead to any other entrance
|
||||
goals:
|
||||
ganon: 5 #beat GT and then Ganon
|
||||
fast_ganon: 4 # Just kill Ganon
|
||||
dungeons: 1 # All Dungeons, including GT, and Agahnims Tower
|
||||
pedestal: 0 # Pull the win out of the Pedestal
|
||||
triforce-hunt: 0 # Collect 20 of 30 Triforce pieces then hand them in in front of Hyrule Castle
|
||||
tower_open: # Crystals required to open GT
|
||||
'0': 0
|
||||
'1': 0
|
||||
'2': 0
|
||||
'3': 0
|
||||
'4': 0
|
||||
'5': 0
|
||||
'6': 0
|
||||
'7': 0
|
||||
random: 1
|
||||
ganon_open: # Crystals required to hurt Ganon
|
||||
'0': 0
|
||||
'1': 0
|
||||
'2': 0
|
||||
'3': 0
|
||||
'4': 0
|
||||
'5': 0
|
||||
'6': 0
|
||||
'7': 0
|
||||
random: 1
|
||||
world_state:
|
||||
standard: 1 # Do standard escape to bring Zelda to Sanctuary
|
||||
open: 9 # Start with the ability to skip the standard opening and go where you want
|
||||
inverted: 0 # You start in the Dark World, the Light World has changes to it's Map and requires a Moon Pearl to not be Bunny
|
||||
retro: 0 # Keys are universal, you have to buy a quiver, there are take any caves and some other changes. Makes it more like Z1
|
||||
hints:
|
||||
'on': 1 # Hint tiles can give useful item location hints on occasion
|
||||
'off': 0 # You get gameplay hints, but not location/item hints
|
||||
weapons: # this means swords
|
||||
randomized: 5 # Your swords can be anywhere
|
||||
assured: 2 # You start with a sword, the rest are anywhere
|
||||
vanilla: 3 # Your swords are in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
|
||||
swordless: 0 # You don't have a sword. A hammer can be used like a Master Sword in certain situations
|
||||
item_pool:
|
||||
normal: 1
|
||||
hard: 0
|
||||
expert: 0
|
||||
crowd_control: 0
|
||||
item_functionality:
|
||||
normal: 1
|
||||
hard: 0
|
||||
expert: 0
|
||||
boss_shuffle:
|
||||
none: 1
|
||||
simple: 0 # existing bosses gets shuffled around
|
||||
full: 0 # all bosses exist once, except 3 can appear twice
|
||||
random: 0 # any boss can appear any number of times
|
||||
enemy_shuffle:
|
||||
none: 1
|
||||
shuffled: 0 # enemies get shuffled around
|
||||
random: 0 # any enemy can appear any number of times
|
||||
enemy_damage:
|
||||
default: 1
|
||||
shuffled: 0 # damage tables get shuffled, however armor effects are not
|
||||
random: 0 # all damages are completely shuffled, including armor effects, making it possible red mail is worse than green
|
||||
enemy_health:
|
||||
default: 1
|
||||
easy: 0
|
||||
hard: 0
|
||||
expert: 0
|
||||
pot_shuffle: # Shuffle pots, their contents and whatever is hiding under them. Broken with any door shuffle that is not vanilla, do not combine
|
||||
on: 0
|
||||
off: 1
|
||||
beemizer: # replace items with bees, that will attack you
|
||||
0: 1
|
||||
1: 0 # max 15 hearts
|
||||
2: 0 # max 10 hearts
|
||||
3: 0
|
||||
4: 0
|
||||
timer:
|
||||
none: 1
|
||||
timed: 0
|
||||
timed_ohko: 0
|
||||
ohko: 0
|
||||
timed_countdown: 0
|
||||
display: 0
|
||||
remote_items: # Warning: currently broken. Stores all your items on the server, effectively sending them to you as if another player picked it up
|
||||
on: 0 # intended for racing, as the item information is missing from the ROM
|
||||
off: 1
|
||||
rom:
|
||||
sprite:
|
||||
random: 1
|
||||
randomonhit: 1
|
||||
link: 1 # to get other sprite names, open up gui/Creator, select a sprite and write down the sprite name as it is there
|
||||
disablemusic: off # turn on for V30 MSU packs
|
||||
extendedmsu: off #turn on to have V31 extended MSU support
|
||||
quickswap:
|
||||
on: 1 # press L/R to swap items without opening the menu
|
||||
off: 0
|
||||
menuspeed:
|
||||
normal: 1
|
||||
instant: 0
|
||||
double: 0
|
||||
triple: 0
|
||||
quadruple: 0
|
||||
half: 0
|
||||
heartcolor:
|
||||
red: 1
|
||||
blue: 1
|
||||
green: 1
|
||||
yellow: 1
|
||||
random: 0
|
||||
heartbeep:
|
||||
double: 0
|
||||
normal: 1
|
||||
half: 0
|
||||
quarter: 0
|
||||
off: 0
|
||||
ow_palettes:
|
||||
default: 1
|
||||
random: 1 # shuffle the palette of overworld colors
|
||||
blackout: 0 # makes everything blank, making it almost a blind playthrough
|
||||
uw_palettes:
|
||||
default: 1
|
||||
random: 1 # shuffle the palette of dungeon/cave colors
|
||||
blackout: 0 # makes everything blank, making it almost a blind playthrough
|
|
@ -0,0 +1,56 @@
|
|||
general_options:
|
||||
#File name of the v1.0 J rom
|
||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||
#options for MultiServer
|
||||
#null means nothing, for the server this means to default the value
|
||||
#these overwrite command line arguments!
|
||||
server_options:
|
||||
host: null
|
||||
port: 38281
|
||||
password: null
|
||||
multidata: null
|
||||
savefile: null
|
||||
disable_save: false
|
||||
loglevel: "info"
|
||||
#automatically forward the port that is used, then close that port after 24 hours
|
||||
port_forward: false
|
||||
#Disallow !getitem. Old /getitem cannot be blocked this way
|
||||
disable_item_cheat: false
|
||||
#Client hint system
|
||||
#points given to player for each acquired item
|
||||
location_check_points: 1
|
||||
#point cost to receive a hint via !hint for players
|
||||
hint_cost: 1000 #set to 0 if you want free hints
|
||||
#options for MultiMystery.py
|
||||
multi_mystery_options:
|
||||
#teams, however, note that there is currently no way to supply names for teams 2+ through MultiMystery
|
||||
teams: 1
|
||||
#Where to place the resulting files
|
||||
output_path: "MultiMystery"
|
||||
#location of your Enemizer CLI, available here: https://github.com/Bonta0/Enemizer/releases
|
||||
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
|
||||
#folder from which the player yaml files are pulled from
|
||||
player_files_path: "Players"
|
||||
#meta file name, within players folder
|
||||
meta_file_path: "meta.yaml"
|
||||
#automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
|
||||
#does nothing if the name is not found
|
||||
#example: player_name = "Berserker"
|
||||
player_name: "" # the hosts name
|
||||
#create a spoiler file
|
||||
create_spoiler: 1
|
||||
#Zip the resulting roms
|
||||
#0 -> Don't
|
||||
#1 -> Create a zip
|
||||
#2 -> Create a zip and delete the ROMs that will be in it, except the hosts (requires player_name to be set correctly)
|
||||
zip_roms: 0
|
||||
# zip diff files, 2-> delete the non-zipped one. Note that diffs are only created if they are placed in a zip.
|
||||
zip_diffs: 2
|
||||
#include the spoiler log in the zip, 2 -> delete the non-zipped one
|
||||
zip_spoiler: 0
|
||||
#include the multidata file in the zip, 2 -> delete the non-zipped one, which also means the server won't autostart
|
||||
zip_multidata: 0
|
||||
#zip algorithm to use
|
||||
zip_format: 1 # 1 -> zip, 2 -> 7z, 3->bz2
|
||||
#create roms flagged as race roms
|
||||
race: 0
|
|
@ -0,0 +1,47 @@
|
|||
# this file has to be in the Players folder to take effect, keeping the "meta.yaml" name.
|
||||
# A meta file rolls its own set of options first
|
||||
# the result will then overwrite each player's option in that particular field
|
||||
# for example, if a meta.yaml fast_ganon result is rolled, every player will have that fast_ganon goal
|
||||
# there is the special case of null, which ignores that part of the meta.yaml,
|
||||
# allowing for a chance for that meta to not take effect
|
||||
# players can also have a meta_ignore option to ignore specific options
|
||||
# example of ignore that would be in a player's file:
|
||||
# meta_ignore:
|
||||
# world_state:
|
||||
# inverted
|
||||
#this means, if world_state is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead.
|
||||
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
|
||||
goals:
|
||||
ganon: 10
|
||||
fast_ganon: 25
|
||||
dungeons: 5
|
||||
pedestal: 10
|
||||
triforce-hunt: 1
|
||||
null: 0 # maintain individual goals
|
||||
world_state:
|
||||
standard: 10
|
||||
open: 60
|
||||
inverted: 10
|
||||
retro: 10
|
||||
null: 10 # maintain individual world states
|
||||
tower_open:
|
||||
'0': 8
|
||||
'1': 7
|
||||
'2': 6
|
||||
'3': 5
|
||||
'4': 4
|
||||
'5': 3
|
||||
'6': 2
|
||||
'7': 1
|
||||
random: 10 # a different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
|
||||
ganon_open:
|
||||
'0': 3
|
||||
'1': 4
|
||||
'2': 5
|
||||
'3': 6
|
||||
'4': 7
|
||||
'5': 8
|
||||
'6': 9
|
||||
'7': 10
|
||||
random: 5 # this will mean differing completion times. But leaving it for that surprise effect
|
||||
#do not use meta rom options at this time.
|
|
@ -0,0 +1,7 @@
|
|||
colorama>=0.4.3
|
||||
websockets>=8.1
|
||||
PyYAML>=5.3.1
|
||||
fuzzywuzzy>=0.18.0
|
||||
bsdiff4>=1.1.9
|
||||
upnpy>=1.1.5
|
||||
prompt_toolkit>=3.0.5
|
|
@ -0,0 +1,105 @@
|
|||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
|
||||
import cx_Freeze
|
||||
|
||||
is_64bits = sys.maxsize > 2 ** 32
|
||||
|
||||
folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(),
|
||||
version=sysconfig.get_python_version())
|
||||
buildfolder = Path("build", folder)
|
||||
sbuildfolder = str(buildfolder)
|
||||
libfolder = Path(buildfolder, "lib")
|
||||
library = Path(libfolder, "library.zip")
|
||||
print("Outputting to: " + str(buildfolder))
|
||||
build_resources = "exe_resources"
|
||||
compress = False
|
||||
holoviews = False
|
||||
from hashlib import sha3_512
|
||||
import base64
|
||||
|
||||
def _threaded_hash(filepath):
|
||||
hasher = sha3_512()
|
||||
hasher.update(open(filepath, "rb").read())
|
||||
return base64.b85encode(hasher.digest()).decode()
|
||||
|
||||
os.makedirs(buildfolder, exist_ok=True)
|
||||
|
||||
def manifest_creation():
|
||||
hashes = {}
|
||||
manifestpath = os.path.join(buildfolder, "manifest.json")
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
pool = ThreadPoolExecutor()
|
||||
for dirpath, dirnames, filenames in os.walk(buildfolder):
|
||||
for filename in filenames:
|
||||
path = os.path.join(dirpath, filename)
|
||||
hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path)
|
||||
import json
|
||||
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds")}
|
||||
manifest["hashes"] = {path: hash.result() for path, hash in hashes.items()}
|
||||
json.dump(manifest, open(manifestpath, "wt"), indent=4)
|
||||
print("Created Manifest")
|
||||
|
||||
scripts = {"MultiClient.py" : "BerserkerMultiClient",
|
||||
"MultiMystery.py" : "BerserkerMultiMystery",
|
||||
"MultiServer.py" : "BerserkerMultiServer",
|
||||
"gui.py" : "BerserkerMultiCreator",
|
||||
"Mystery.py" : "BerserkerMystery"}
|
||||
|
||||
exes = []
|
||||
|
||||
for script, scriptname in scripts.items():
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script=script,
|
||||
targetName=scriptname + ("" if sys.platform == "linux" else ".exe"))
|
||||
)
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
buildtime = datetime.datetime.now()
|
||||
|
||||
cx_Freeze.setup(
|
||||
name="HonorarPlus",
|
||||
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
|
||||
description="HonorarPlus",
|
||||
executables=exes,
|
||||
options={
|
||||
"build_exe": {
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": [],
|
||||
"include_files": [],
|
||||
"include_msvcr": True,
|
||||
"replace_paths": [("*", "")],
|
||||
"optimize": 2,
|
||||
"build_exe": buildfolder
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
def installfile(path):
|
||||
lbuildfolder = buildfolder
|
||||
print('copying', path, '->', lbuildfolder)
|
||||
if path.is_dir():
|
||||
lbuildfolder /= path.name
|
||||
if lbuildfolder.is_dir():
|
||||
shutil.rmtree(lbuildfolder)
|
||||
shutil.copytree(path, lbuildfolder)
|
||||
elif path.is_file():
|
||||
shutil.copy(path, lbuildfolder)
|
||||
else:
|
||||
print('Warning,', path, 'not found')
|
||||
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "QUsb2Snes", "meta.yaml"]
|
||||
|
||||
for data in extra_data:
|
||||
installfile(Path(data))
|
||||
|
||||
|
||||
manifest_creation()
|
Loading…
Reference in New Issue