Merge branch 'DoorDev' into EdgeWork

This commit is contained in:
aerinon
2020-03-11 15:09:51 -06:00
58 changed files with 4313 additions and 2043 deletions

6
.gitignore vendored
View File

@@ -16,7 +16,13 @@ README.html
EnemizerCLI/
.mypy_cache/
RaceRom.py
upx/
weights/
settings.json
working_dirs.json
*.exe
venv
test

View File

@@ -21,6 +21,9 @@ def adjust(args):
else:
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
if not hasattr(args,"sprite"):
args.sprite = None
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
rom.write_to_file(output_path('%s.sfc' % outfilebase))

View File

@@ -41,7 +41,7 @@ class World(object):
self.shuffle_bonk_prizes = False
self.light_world_light_cone = False
self.dark_world_light_cone = False
self.clock_mode = 'off'
self.clock_mode = 'none'
self.rupoor_cost = 10
self.aga_randomness = True
self.lock_aga_door_in_escape = False
@@ -769,6 +769,16 @@ class CollectionState(object):
else:
self.prog_items.add(('Bow', item.player))
changed = True
elif 'Armor' in item.name:
if self.has('Red Mail', item.player):
pass
elif self.has('Blue Mail', item.player):
self.prog_items.add(('Red Mail', item.player))
changed = True
else:
self.prog_items.add(('Blue Mail', item.player))
changed = True
elif item.name.startswith('Bottle'):
if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
self.prog_items.add((item.name, item.player))

View File

@@ -183,6 +183,10 @@ def place_bosses(world, player):
raise FillError('Could not place boss for location %s' % loc_text)
bosses.remove(boss)
# GT Bosses can move dungeon - find the real dungeon to place them in
if level:
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
elif world.boss_shuffle[player] == "chaos": #all bosses chosen at random
@@ -193,5 +197,9 @@ def place_bosses(world, player):
except IndexError:
raise FillError('Could not place boss for location %s' % loc_text)
# GT Bosses can move dungeon - find the real dungeon to place them in
if level:
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)

508
CLI.py Normal file
View File

@@ -0,0 +1,508 @@
import argparse
import copy
import json
import os
import logging
import random
import textwrap
import shlex
import sys
from Main import main
from Utils import is_bundled, close_console
from Fill import FillError
import classes.constants as CONST
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
# get settings
settings = get_settings()
# we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args(argv)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', default=defval(settings["create_spoiler"] != 0), help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default=defval(settings["logic"]), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
help='''\
Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches:
Minor Glitches: May require Fake Flippers, Bunny Revival
and Dark Room Navigation.
No Logic: Distribute items without regard for
item requirements.
''')
parser.add_argument('--mode', default=defval(settings["mode"]), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
help='''\
Select game mode. (default: %(default)s)
Open: World starts with Zelda rescued.
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.
Inverted: Starting locations are Dark Sanctuary in West Dark
World or at Link's House, which is shuffled freely.
Requires the moon pearl to be Link in the Light World
instead of a bunny.
''')
parser.add_argument('--swords', default=defval(settings["swords"]), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
help='''\
Select sword placement. (default: %(default)s)
Random: All swords placed randomly.
Assured: Start game with a sword already.
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon.
Ether and Bombos Tablet can be activated with Hammer
(and Book). Bombos pads have been added in Ice
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
parser.add_argument('--goal', default=defval(settings["goal"]), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
help='''\
Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then
defeat Ganon.
Crystals: Collect all crystals then defeat Ganon.
Pedestal: Places the Triforce at the Master Sword Pedestal.
All Dungeons: Collect all crystals, pendants, beat both
Agahnim fights and then defeat Ganon.
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game.
''')
parser.add_argument('--difficulty', default=defval(settings["difficulty"]), 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=defval(settings["item_functionality"]), 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=defval(settings["timer"]), 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.
Display: Displays a timer but does not affect
the itempool.
Timed: Starts with clock at zero. Green Clocks
subtract 4 minutes (Total: 20), Blue Clocks
subtract 2 minutes (Total: 10), Red Clocks add
2 minutes (Total: 10). Winner is player with
lowest time at the end.
Timed OHKO: Starts clock at 10 minutes. Green Clocks add
5 minutes (Total: 25). As long as clock is at 0,
Link will die in one hit.
OHKO: Like Timed OHKO, but no clock items are present
and the clock is permenantly at zero.
Timed Countdown: Starts with clock at 40 minutes. Same clocks as
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
parser.add_argument('--progressive', default=defval(settings["progressive"]), 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
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default=defval(settings["algorithm"]), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
help='''\
Select item filling algorithm. (default: %(default)s
balanced: vt26 derivative that aims to strike a balance between
the overworld heavy vt25 and the dungeon heavy vt26
algorithm.
vt26: Shuffle items and place them in a random location
that it is not impossible to be in. This includes
dungeon keys and items.
vt25: Shuffle items and place them in a random location
that it is not impossible to be in.
vt21: Unbiased in its selection, but has tendency to put
Ice Rod in Turtle Rock.
vt22: Drops off stale locations after 1/3 of progress
items were placed to try to circumvent vt21\'s
shortcomings.
Freshness: Keep track of stale locations (ones that cannot be
reached yet) and decrease likeliness of selecting
them the more often they were found unreachable.
Flood: Push out items starting from Link\'s House and
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval(settings["shuffle"]), 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
multi-entrance caves to one world.
Simple: Shuffle Dungeon Entrances/Exits between each other
and keep all 4-entrance dungeons confined to one
location. All caves outside of death mountain are
shuffled in pairs and matched by original type.
Restricted: Use Dungeons shuffling from Simple but freely
connect remaining entrances.
Crossed: Mix cave and dungeon entrances freely while allowing
caves to cross between worlds.
Insanity: Decouple entrances and exits from each other and
shuffle them freely. Caves that used to be single
entrance will still exit to the same location from
which they are entered.
Vanilla: All entrances are in the same locations they were
in the base game.
Legacy shuffles preserve behavior from older versions of the
entrance randomizer including significant technical limitations.
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--door_shuffle', default=defval(settings["door_shuffle"]), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed'],
help='''\
Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon.
(Not yet implemented)
Crossed: Doors are mixed between all dungeons.
(Not yet implemented)
Vanilla: All doors are connected the same way they were in the
base game.
''')
parser.add_argument('--experimental', default=defval(settings["experimental"] != 0), help='Enable experimental features', action='store_true')
parser.add_argument('--dungeon_counters', default=defval(settings["dungeon_counters"]), help='Enable dungeon chest counters', const='off', nargs='?', choices=['off', 'on', 'pickup', 'default'])
parser.add_argument('--crystals_ganon', default=defval(settings["crystals_ganon"]), 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.
This setting does not apply when the all dungeons goal is
selected. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--crystals_gt', default=defval(settings["crystals_gt"]), 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('--openpyramid', default=defval(settings["openpyramid"] != 0), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
''', action='store_true')
parser.add_argument('--rom', default=defval(settings["rom"]), 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', default=defval(int(settings["seed"]) if settings["seed"] != "" and settings["seed"] is not None else None), help='Define seed number to generate.', type=int)
parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else None), help='''\
Use to batch generate multiple seeds with same settings.
If --seed is provided, it will be used for the first seed, then
used to derive the next seed (i.e. generating 10 seeds with
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', default=defval(settings["fastmenu"]), 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', default=defval(settings["quickswap"] != 0), help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', default=defval(settings["disablemusic"] != 0), help='Disables game music.', action='store_true')
parser.add_argument('--mapshuffle', default=defval(settings["mapshuffle"] != 0), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--compassshuffle', default=defval(settings["compassshuffle"] != 0), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', default=defval(settings["keyshuffle"] != 0), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', default=defval(settings["bigkeyshuffle"] != 0), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keysanity', default=defval(settings["keysanity"] != 0), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(settings["retro"] != 0), 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('--startinventory', default=defval(settings["startinventory"]), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--usestartinventory', default=defval(settings["usestartinventory"] != 0), help='Not supported.')
parser.add_argument('--custom', default=defval(settings["custom"] != 0), help='Not supported.')
parser.add_argument('--customitemarray', default={}, help='Not supported.')
parser.add_argument('--accessibility', default=defval(settings["accessibility"]), 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', default=defval(settings["hints"] != 0), 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=defval(settings["shuffleganon"] != 0))
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=defval(settings["heartbeep"]), 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=defval(settings["heartcolor"]), 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(settings["ow_palettes"]), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval(settings["uw_palettes"]), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', default=defval(settings["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,
or 0x7078 (28792) bytes including palette data.
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--suppress_rom', default=defval(settings["suppress_rom"] != 0), help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--jsonout', action='store_true', help='''\
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=defval(settings["skip_playthrough"] != 0))
parser.add_argument('--enemizercli', default=defval(settings["enemizercli"]))
parser.add_argument('--shufflebosses', default=defval(settings["shufflebosses"]), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=defval(settings["shuffleenemies"]), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval(settings["enemy_health"]), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default=defval(settings["enemy_damage"]), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepots', default=defval(settings["shufflepots"] != 0), action='store_true')
parser.add_argument('--beemizer', default=defval(settings["beemizer"]), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--remote_items', default=defval(settings["remote_items"] != 0), action='store_true')
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(settings["names"]))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath', default=defval(settings["outputpath"]))
parser.add_argument('--race', default=defval(settings["race"] != 0), action='store_true')
parser.add_argument('--saveonexit', default=defval(settings["saveonexit"]), choices=['never', 'ask', 'always'])
parser.add_argument('--outputname')
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.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', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items']:
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 get_settings():
# set default settings
settings = {
"retro": False,
"mode": "open",
"logic": "noglitches",
"goal": "ganon",
"crystals_gt": "7",
"crystals_ganon": "7",
"swords": "random",
"difficulty": "normal",
"item_functionality": "normal",
"timer": "none",
"progressive": "on",
"accessibility": "items",
"algorithm": "balanced",
"openpyramid": False,
"shuffleganon": False,
"shuffle": "vanilla",
"shufflepots": False,
"shuffleenemies": "none",
"shufflebosses": "none",
"enemy_damage": "default",
"enemy_health": "default",
"enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"),
"mapshuffle": False,
"compassshuffle": False,
"keyshuffle": False,
"bigkeyshuffle": False,
"keysanity": False,
"door_shuffle": "basic",
"experimental": False,
"dungeon_counters": "default",
"multi": 1,
"names": "",
"hints": True,
"disablemusic": False,
"quickswap": False,
"heartcolor": "red",
"heartbeep": "normal",
"sprite": None,
"fastmenu": "normal",
"ow_palettes": "default",
"uw_palettes": "default",
"create_spoiler": False,
"skip_playthrough": False,
"suppress_rom": False,
"usestartinventory": False,
"custom": False,
"rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"),
"seed": None,
"count": None,
"startinventory": "",
"beemizer": 0,
"remote_items": False,
"race": False,
"customitemarray": {
"bow": 0,
"progressivebow": 2,
"boomerang": 1,
"redmerang": 1,
"hookshot": 1,
"mushroom": 1,
"powder": 1,
"firerod": 1,
"icerod": 1,
"bombos": 1,
"ether": 1,
"quake": 1,
"lamp": 1,
"hammer": 1,
"shovel": 1,
"flute": 1,
"bugnet": 1,
"book": 1,
"bottle": 4,
"somaria": 1,
"byrna": 1,
"cape": 1,
"mirror": 1,
"boots": 1,
"powerglove": 0,
"titansmitt": 0,
"progressiveglove": 2,
"flippers": 1,
"pearl": 1,
"heartpiece": 24,
"heartcontainer": 10,
"sancheart": 1,
"sword1": 0,
"sword2": 0,
"sword3": 0,
"sword4": 0,
"progressivesword": 4,
"shield1": 0,
"shield2": 0,
"shield3": 0,
"progressiveshield": 3,
"mail2": 0,
"mail3": 0,
"progressivemail": 2,
"halfmagic": 1,
"quartermagic": 0,
"bombsplus5": 0,
"bombsplus10": 0,
"arrowsplus5": 0,
"arrowsplus10": 0,
"arrow1": 1,
"arrow10": 12,
"bomb1": 0,
"bomb3": 16,
"bomb10": 1,
"rupee1": 2,
"rupee5": 4,
"rupee20": 28,
"rupee50": 7,
"rupee100": 1,
"rupee300": 5,
"blueclock": 0,
"greenclock": 0,
"redclock": 0,
"silversupgrade": 0,
"generickeys": 0,
"triforcepieces": 0,
"triforcepiecesgoal": 0,
"triforce": 0,
"rupoor": 0,
"rupoorcost": 10
},
"randomSprite": False,
"outputpath": os.path.join("."),
"saveonexit": "ask",
"startinventoryarray": {}
}
if sys.platform.lower().find("windows"):
settings["enemizercli"] += ".exe"
# read saved settings file if it exists and set these
settings_path = os.path.join(".", "resources", "user", "settings.json")
if os.path.exists(settings_path):
with(open(settings_path)) as json_file:
data = json.load(json_file)
for k, v in data.items():
settings[k] = v
return settings
def get_args_priority(settings_args, gui_args, cli_args):
args = {}
args["settings"] = get_settings() if settings_args is None else settings_args
args["gui"] = {} if gui_args is None else gui_args
args["cli"] = cli_args
args["load"] = args["settings"]
if args["gui"] is not None:
for k in args["gui"]:
if k not in args["load"] or args["load"][k] != args["gui"]:
args["load"][k] = args["gui"][k]
if args["cli"] is None:
args["cli"] = {}
cli = vars(parse_arguments(None))
for k, v in cli.items():
if isinstance(v, dict) and 1 in v:
args["cli"][k] = v[1]
else:
args["cli"][k] = v
load_doesnt_have_key = k not in args["load"]
different_val = (k in args["load"] and k in args["cli"]) and (args["load"][k] != args["cli"][k])
cli_has_empty_dict = k in args["cli"] and isinstance(args["cli"][k], dict) and len(args["cli"][k]) == 0
if load_doesnt_have_key or different_val:
if not cli_has_empty_dict:
args["load"][k] = args["cli"][k]
return args

View File

@@ -50,9 +50,9 @@ def link_doors(world, player):
connect_one_way(world, ent, ext, player)
vanilla_key_logic(world, player)
elif world.doorShuffle[player] == 'basic':
if not world.experimental[player]:
for entrance, ext in open_edges:
connect_two_way(world, entrance, ext, player)
# if not world.experimental[player]:
for entrance, ext in open_edges:
connect_two_way(world, entrance, ext, player)
within_dungeon(world, player)
elif world.doorShuffle[player] == 'crossed':
for entrance, ext in open_edges:
@@ -139,7 +139,7 @@ def vanilla_key_logic(world, player):
enabled_entrances = {}
sector_queue = deque(builders)
last_key = None
last_key, loops = None, 0
while len(sector_queue) > 0:
builder = sector_queue.popleft()
@@ -147,12 +147,14 @@ def vanilla_key_logic(world, player):
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
origin_list_sans_drops = remove_drop_origins(origin_list)
if len(origin_list_sans_drops) <= 0:
if last_key == builder.name:
raise Exception('Infinte loop detected %s' % builder.name)
if last_key == builder.name or loops > 1000:
origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name
raise Exception('Infinite loop detected for "%s" located at %s' % builder.name, origin_name)
sector_queue.append(builder)
last_key = builder.name
loops += 1
else:
find_new_entrances(builder.master_sector, connections, potentials, enabled_entrances, world, player)
find_new_entrances(builder.master_sector, entrances_map, connections, potentials, enabled_entrances, world, player)
start_regions = convert_regions(origin_list, world, player)
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
key_layout = build_key_layout(builder, start_regions, doors, world, player)
@@ -165,7 +167,7 @@ def vanilla_key_logic(world, player):
world.key_logic[player][builder.name] = key_layout.key_logic
log_key_logic(builder.name, key_layout.key_logic)
last_key = None
if world.shuffle[player] == 'vanilla':
if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player]:
validate_vanilla_key_logic(world, player)
@@ -202,8 +204,7 @@ def connect_simple_door(world, exit_name, region_name, player):
d.dest = region
def connect_door_only(world, exit_name, region_name, player):
region = world.get_region(region_name, player)
def connect_door_only(world, exit_name, region, player):
d = world.check_for_door(exit_name, player)
if d is not None:
d.dest = region
@@ -351,7 +352,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
entrances_map, potentials, connections = connections_tuple
enabled_entrances = {}
sector_queue = deque(dungeon_builders.values())
last_key = None
last_key, loops = None, 0
while len(sector_queue) > 0:
builder = sector_queue.popleft()
split_dungeon = builder.name.startswith('Desert Palace') or builder.name.startswith('Skull Woods')
@@ -362,14 +363,16 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
origin_list_sans_drops = remove_drop_origins(origin_list)
if len(origin_list_sans_drops) <= 0 or name == "Turtle Rock" and not validate_tr(builder, origin_list_sans_drops, world, player):
if last_key == builder.name:
raise Exception('Infinte loop detected %s' % builder.name)
if last_key == builder.name or loops > 1000:
origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name
raise Exception('Infinite loop detected for "%s" located at %s' % builder.name, origin_name)
sector_queue.append(builder)
last_key = builder.name
loops += 1
else:
logging.getLogger('').info('Generating dungeon: %s', builder.name)
ds = generate_dungeon(builder, origin_list_sans_drops, split_dungeon, world, player)
find_new_entrances(ds, connections, potentials, enabled_entrances, world, player)
find_new_entrances(ds, entrances_map, connections, potentials, enabled_entrances, world, player)
ds.name = name
builder.master_sector = ds
builder.layout_starts = origin_list if len(builder.entrance_list) <= 0 else builder.entrance_list
@@ -433,27 +436,50 @@ def remove_drop_origins(entrance_list):
return [x for x in entrance_list if x not in drop_entrances]
def find_new_entrances(sector, connections, potentials, enabled, world, player):
def find_new_entrances(sector, entrances_map, connections, potentials, enabled, world, player):
for region in sector.regions:
if region.name in connections.keys() and (connections[region.name] in potentials.keys() or connections[region.name].name in world.inaccessible_regions[player]):
new_region = connections[region.name]
if new_region in potentials.keys():
for potential in potentials.pop(new_region):
enabled[potential] = (region.name, region.dungeon)
# see if this unexplored region connects elsewhere
queue = deque(new_region.exits)
visited = set()
while len(queue) > 0:
ext = queue.popleft()
visited.add(ext)
region_name = ext.connected_region.name
if region_name in connections.keys() and connections[region_name] in potentials.keys():
for potential in potentials.pop(connections[region_name]):
enabled[potential] = (region.name, region.dungeon)
if ext.connected_region.name in world.inaccessible_regions[player]:
for new_exit in ext.connected_region.exits:
if new_exit not in visited:
queue.append(new_exit)
enable_new_entrances(region, connections, potentials, enabled, world, player)
inverted_aga_check(entrances_map, connections, potentials, enabled, world, player)
def enable_new_entrances(region, connections, potentials, enabled, world, player):
new_region = connections[region.name]
if new_region in potentials.keys():
for potential in potentials.pop(new_region):
enabled[potential] = (region.name, region.dungeon)
# see if this unexplored region connects elsewhere
queue = deque(new_region.exits)
visited = set()
while len(queue) > 0:
ext = queue.popleft()
visited.add(ext)
region_name = ext.connected_region.name
if region_name in connections.keys() and connections[region_name] in potentials.keys():
for potential in potentials.pop(connections[region_name]):
enabled[potential] = (region.name, region.dungeon)
if ext.connected_region.name in world.inaccessible_regions[player]:
for new_exit in ext.connected_region.exits:
if new_exit not in visited:
queue.append(new_exit)
def inverted_aga_check(entrances_map, connections, potentials, enabled, world, player):
if world.mode[player] == 'inverted':
if 'Agahnims Tower' in entrances_map.keys() or aga_tower_enabled(enabled):
for region in list(potentials.keys()):
if region.name == 'Hyrule Castle Ledge':
for r_name in potentials[region]:
new_region = world.get_region(r_name, player)
enable_new_entrances(new_region, connections, potentials, enabled, world, player)
def aga_tower_enabled(enabled):
for region_name, enabled_tuple in enabled.items():
entrance, dungeon = enabled_tuple
if dungeon.name == 'Agahnims Tower':
return True
return False
def within_dungeon_legacy(world, player):
@@ -1357,8 +1383,8 @@ def add_inaccessible_doors(world, player):
# todo: ignore standard mode hyrule castle ledge?
for inaccessible_region in world.inaccessible_regions[player]:
region = world.get_region(inaccessible_region, player)
for exit in region.exits:
create_door(world, player, exit.name, region.name)
for ext in region.exits:
create_door(world, player, ext.name, region.name)
def create_door(world, player, entName, region_name):
@@ -1464,6 +1490,7 @@ def check_for_pinball_fix(state, bad_region, world, player):
@unique
class DROptions(Flag):
NoOptions = 0x00
Eternal_Mini_Bosses = 0x01 # If on, GT minibosses marked as defeated when they try to spawn a heart
Town_Portal = 0x02 # If on, Players will start with mirror scroll
Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required

View File

@@ -1,5 +1,6 @@
import random
import collections
import itertools
from collections import defaultdict, deque
from enum import Enum, unique
import logging
@@ -835,6 +836,8 @@ class ExplorationState(object):
def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.controller:
door = door.controller
if door in key_door_proposal and door not in self.opened_doors:
if not self.in_door_list(door, self.small_doors):
self.append_door_to_list(door, self.small_doors)
@@ -1493,20 +1496,32 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
problem_builders = identify_branching_issues_2(problem_builders)
# step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc.
comb_w_replace = len(dungeon_map) ** len(neutral_choices)
combinations = None
if comb_w_replace <= 1000:
combinations = list(itertools.product(dungeon_map.keys(), repeat=len(neutral_choices)))
random.shuffle(combinations)
tries = 0
while len(polarized_sectors) > 0:
if tries > 100:
if tries > 1000 or (combinations and tries >= len(combinations)):
raise Exception('No valid assignment found. Ref: %s' % next(iter(dungeon_map.keys())))
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices))
valid = []
if combinations:
choices = combinations[tries]
else:
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices))
chosen_sectors = defaultdict(list)
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
if valid_assignment(builder, neutral_choices[i]):
chosen_sectors[choice].extend(neutral_choices[i])
all_valid = True
for name, sector_list in chosen_sectors.items():
if not valid_assignment(dungeon_map[name], sector_list):
all_valid = False
break
if all_valid:
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
for sector in neutral_choices[i]:
assign_sector(sector, builder, polarized_sectors, global_pole)
valid.append(neutral_choices[i])
for c in valid:
neutral_choices.remove(c)
tries += 1
@@ -1957,6 +1972,7 @@ def resolve_equations(builder, sector_list):
# negative benefit transforms (dead end)
def find_priority_equation(equations, current_access):
flex = calc_flex(equations, current_access)
required = calc_required(equations, current_access)
best_profit = None
triplet_candidates = []
local_profit_map = {}
@@ -1977,14 +1993,17 @@ def find_priority_equation(equations, current_access):
else:
triplet_candidates.append((eq, eq_list, sector))
local_profit_map[sector] = best_local_profit
if len(triplet_candidates) == 0:
filtered_candidates = filter_requirements(triplet_candidates, equations, required, current_access)
if len(filtered_candidates) == 0:
filtered_candidates = triplet_candidates
if len(filtered_candidates) == 0:
return None, None, None # can't pay for anything
if len(triplet_candidates) == 1:
return triplet_candidates[0]
if len(filtered_candidates) == 1:
return filtered_candidates[0]
required_candidates = [x for x in triplet_candidates if x[0].required]
required_candidates = [x for x in filtered_candidates if x[0].required]
if len(required_candidates) == 0:
required_candidates = triplet_candidates
required_candidates = filtered_candidates
if len(required_candidates) == 1:
return required_candidates[0]
@@ -2000,6 +2019,46 @@ def find_priority_equation(equations, current_access):
return good_local_candidates[0] # just pick one I guess
def calc_required(equations, current_access):
ttl = 0
for num in current_access.values():
ttl += num
local_profit_map = {}
for sector, eq_list in equations.items():
best_local_profit = None
for eq in eq_list:
profit = eq.profit()
if best_local_profit is None or profit > best_local_profit:
best_local_profit = profit
local_profit_map[sector] = best_local_profit
ttl += best_local_profit
if ttl == 0:
new_lists = {}
for sector, eq_list in equations.items():
if len(eq_list) > 1:
rem_list = []
for eq in eq_list:
if eq.profit() < local_profit_map[sector]:
rem_list.append(eq)
if len(rem_list) > 0:
new_lists[sector] = [x for x in eq_list if x not in rem_list]
for sector, eq_list in new_lists.items():
if len(eq_list) <= 1:
for eq in eq_list:
eq.required = True
equations[sector] = eq_list
required_costs = defaultdict(int)
required_benefits = defaultdict(int)
for sector, eq_list in equations.items():
for eq in eq_list:
if eq.required:
for key, door_list in eq.cost.items():
required_costs[key] += len(door_list)
for key, door_list in eq.benefit.items():
required_benefits[key] += len(door_list)
return required_costs, required_benefits
def calc_flex(equations, current_access):
flex_spending = defaultdict(int)
required_costs = defaultdict(int)
@@ -2013,6 +2072,45 @@ def calc_flex(equations, current_access):
return flex_spending
def filter_requirements(triplet_candidates, equations, required, current_access):
r_costs, r_exits = required
valid_candidates = []
for cand, cand_list, cand_sector in triplet_candidates:
valid = True
if not cand.required:
potential_benefit = defaultdict(int)
potential_costs = defaultdict(int)
for h_type, benefit in current_access.items():
cur_cost = len(cand.cost[h_type])
if benefit - cur_cost > 0:
potential_benefit[h_type] += benefit - cur_cost
for h_type, benefit_list in cand.benefit.items():
potential_benefit[h_type] += len(benefit_list)
for sector, eq_list in equations.items():
if sector == cand_sector:
affected_doors = [d for x in cand.benefit.values() for d in x] + [d for x in cand.cost.values() for d in x]
adj_list = [x for x in eq_list if x.door not in affected_doors]
else:
adj_list = eq_list
for eq in adj_list:
for h_type, benefit_list in eq.benefit.items():
potential_benefit[h_type] += len(benefit_list)
for h_type, cost_list in eq.cost.items():
potential_costs[h_type] += len(cost_list)
for h_type, requirement in r_costs.items():
if requirement > 0 and potential_benefit[h_type] < requirement:
valid = False
break
if valid:
for h_type, requirement in r_exits.items():
if requirement > 0 and potential_costs[h_type] < requirement:
valid = False
break
if valid:
valid_candidates.append((cand, cand_list, cand_sector))
return valid_candidates
def resolve_equation(equation, eq_list, sector, current_access, reached_doors, equations):
for key, door_list in equation.cost.items():
if current_access[key] - len(door_list) < 0:

View File

@@ -8,313 +8,12 @@ import textwrap
import shlex
import sys
from CLI import parse_arguments
from Main import main
from Rom import get_sprite_from_name
from Utils import is_bundled, close_console
from Fill import FillError
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)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
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:
Minor Glitches: May require Fake Flippers, Bunny Revival
and Dark Room Navigation.
No Logic: Distribute items without regard for
item requirements.
''')
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.
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.
Inverted: Starting locations are Dark Sanctuary in West Dark
World or at Link's House, which is shuffled freely.
Requires the moon pearl to be Link in the Light World
instead of a bunny.
''')
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.
Assured: Start game with a sword already.
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon.
Ether and Bombos Tablet can be activated with Hammer
(and Book). Bombos pads have been added in Ice
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
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
defeat Ganon.
Crystals: Collect all crystals then defeat Ganon.
Pedestal: Places the Triforce at the Master Sword Pedestal.
All Dungeons: Collect all crystals, pendants, beat both
Agahnim fights and then defeat Ganon.
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game.
''')
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=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=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.
Display: Displays a timer but does not affect
the itempool.
Timed: Starts with clock at zero. Green Clocks
subtract 4 minutes (Total: 20), Blue Clocks
subtract 2 minutes (Total: 10), Red Clocks add
2 minutes (Total: 10). Winner is player with
lowest time at the end.
Timed OHKO: Starts clock at 10 minutes. Green Clocks add
5 minutes (Total: 25). As long as clock is at 0,
Link will die in one hit.
OHKO: Like Timed OHKO, but no clock items are present
and the clock is permenantly at zero.
Timed Countdown: Starts with clock at 40 minutes. Same clocks as
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
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
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
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 derivative that aims to strike a balance between
the overworld heavy vt25 and the dungeon heavy vt26
algorithm.
vt26: Shuffle items and place them in a random location
that it is not impossible to be in. This includes
dungeon keys and items.
vt25: Shuffle items and place them in a random location
that it is not impossible to be in.
vt21: Unbiased in its selection, but has tendency to put
Ice Rod in Turtle Rock.
vt22: Drops off stale locations after 1/3 of progress
items were placed to try to circumvent vt21\'s
shortcomings.
Freshness: Keep track of stale locations (ones that cannot be
reached yet) and decrease likeliness of selecting
them the more often they were found unreachable.
Flood: Push out items starting from Link\'s House and
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval('vanilla'), 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
multi-entrance caves to one world.
Simple: Shuffle Dungeon Entrances/Exits between each other
and keep all 4-entrance dungeons confined to one
location. All caves outside of death mountain are
shuffled in pairs and matched by original type.
Restricted: Use Dungeons shuffling from Simple but freely
connect remaining entrances.
Crossed: Mix cave and dungeon entrances freely while allowing
caves to cross between worlds.
Insanity: Decouple entrances and exits from each other and
shuffle them freely. Caves that used to be single
entrance will still exit to the same location from
which they are entered.
Vanilla: All entrances are in the same locations they were
in the base game.
Legacy shuffles preserve behavior from older versions of the
entrance randomizer including significant technical limitations.
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--door_shuffle', default=defval('basic'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed'],
help='''\
Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon.
(Not yet implemented)
Crossed: Doors are mixed between all dungeons.
(Not yet implemented)
Vanilla: All doors are connected the same way they were in the
base game.
''')
parser.add_argument('--experimental', default=defval(False), help='Enable experimental features', action='store_true')
parser.add_argument('--dungeon_counters', default=defval(False), help='Enable dungeon chest counters', action='store_true')
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.
This setting does not apply when the all dungeons goal is
selected. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
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('--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.
If --seed is provided, it will be used for the first seed, then
used to derive the next seed (i.e. generating 10 seeds with
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
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', default=defval(False), help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', default=defval(False), help='Disables game music.', 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('--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', 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=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=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=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,
or 0x7078 (28792) bytes including palette data.
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--jsonout', action='store_true', help='''\
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=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')
parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
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.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', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items']:
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)
@@ -360,7 +59,12 @@ def start():
seed = random.randint(0, 999999999)
for fail in failures:
logger.info('%s seed failed with: %s', fail[1], fail[0])
logger.info('Generation fail rate: %f%%', 100*len(failures)/args.count)
fail_rate = 100 * len(failures) / args.count
success_rate = 100 * (args.count - len(failures)) / args.count
fail_rate = str(fail_rate).split('.')
success_rate = str(success_rate).split('.')
logger.info('Generation fail rate: ' + str(fail_rate[0] ).rjust(3, " ") + '.' + str(fail_rate[1] ).ljust(6, '0') + '%')
logger.info('Generation success rate: ' + str(success_rate[0]).rjust(3, " ") + '.' + str(success_rate[1]).ljust(6, '0') + '%')
else:
main(seed=args.seed, args=args)

59
DungeonRandomizer.spec Normal file
View File

@@ -0,0 +1,59 @@
# -*- mode: python -*-
block_cipher = None
console = True
def recurse_for_py_files(names_so_far):
returnvalue = []
for name in os.listdir(os.path.join(*names_so_far)):
if name != "__pycache__":
subdir_name = os.path.join(*names_so_far, name)
if os.path.isdir(subdir_name):
new_name_list = names_so_far + [name]
for filename in os.listdir(os.path.join(*new_name_list)):
base_file,file_extension = os.path.splitext(filename)
if file_extension == ".py":
new_name = ".".join(new_name_list+[base_file])
if not new_name in returnvalue:
returnvalue.append(new_name)
returnvalue.extend(recurse_for_py_files(new_name_list))
returnvalue.append("PIL._tkinter_finder") #Linux needs this
return returnvalue
hiddenimports = []
a = Analysis(['DungeonRandomizer.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=hiddenimports,
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller
excluded_binaries = [
'VCRUNTIME140.dll',
'msvcp140.dll',
'mfc140u.dll']
a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries])
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='DungeonRandomizer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=console ) # <--- change this to True to enable command prompt when the app runs

View File

@@ -132,7 +132,6 @@ def fill_dungeons_restrictive(world, shuffled_locations):
# 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
@@ -146,7 +145,8 @@ def fill_dungeons_restrictive(world, shuffled_locations):
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, True)
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items,
keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, single_player_placement=True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],

View File

@@ -2298,8 +2298,6 @@ Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
'Death Mountain Return Cave (East)',
'Death Mountain Return Cave (West)',
'Spectacle Rock Cave Peak',
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)',
'Paradox Cave (Bottom)',
'Paradox Cave (Middle)',
'Paradox Cave (Top)',

11
Fill.py
View File

@@ -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, single_player_placement = False):
def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = None, single_player_placement = False):
def sweep_from_pool():
new_state = base_state.copy()
for item in itempool:
@@ -202,7 +202,7 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place
test_state = maximum_exploration_state
if (not single_player_placement or location.player == item_to_place.player)\
and location.can_fill(test_state, item_to_place, perform_access_check)\
and valid_key_placement(item_to_place, location, itempool, world):
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
spot_to_fill = location
break
elif item_to_place.smallkey or item_to_place.bigkey:
@@ -225,7 +225,7 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place
itempool.extend(unplaced_items)
def valid_key_placement(item, location, itempool, world):
def valid_key_placement(item, location, itempool, world):
if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player]:
return True
dungeon = location.parent_region.dungeon
@@ -233,7 +233,7 @@ def valid_key_placement(item, location, itempool, world):
if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name):
return True
key_logic = world.key_logic[item.player][dungeon.name]
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name])
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
return key_logic.check_placement(unplaced_keys)
else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
@@ -290,7 +290,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# todo: crossed
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0)
fill_restrictive(world, world.state, fill_locations, progitempool)
fill_restrictive(world, world.state, fill_locations, progitempool,
keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players+1)})
random.shuffle(fill_locations)

1684
Gui.py

File diff suppressed because it is too large Load Diff

59
Gui.spec Normal file
View File

@@ -0,0 +1,59 @@
# -*- mode: python -*-
block_cipher = None
console = True
def recurse_for_py_files(names_so_far):
returnvalue = []
for name in os.listdir(os.path.join(*names_so_far)):
if name != "__pycache__":
subdir_name = os.path.join(*names_so_far, name)
if os.path.isdir(subdir_name):
new_name_list = names_so_far + [name]
for filename in os.listdir(os.path.join(*new_name_list)):
base_file,file_extension = os.path.splitext(filename)
if file_extension == ".py":
new_name = ".".join(new_name_list+[base_file])
if not new_name in returnvalue:
returnvalue.append(new_name)
returnvalue.extend(recurse_for_py_files(new_name_list))
returnvalue.append("PIL._tkinter_finder") #Linux needs this
return returnvalue
hiddenimports = []
a = Analysis(['Gui.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=hiddenimports,
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller
excluded_binaries = [
'VCRUNTIME140.dll',
'msvcp140.dll',
'mfc140u.dll']
a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries])
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Gui',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=console ) # <--- change this to True to enable command prompt when the app runs

View File

@@ -9,6 +9,8 @@ from EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive
from Items import ItemFactory
import classes.constants as CONST
#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.
@@ -124,6 +126,58 @@ difficulties = {
),
}
def get_custom_array_key(item):
label_switcher = {
"silverarrow": "silversupgrade",
"blueboomerang": "boomerang",
"redboomerang": "redmerang",
"ocarina": "flute",
"bugcatchingnet": "bugnet",
"bookofmudora": "book",
"pegasusboots": "boots",
"titansmitts": "titansmitt",
"pieceofheart": "heartpiece",
"bossheartcontainer": "heartcontainer",
"sanctuaryheartcontainer": "sancheart",
"mastersword": "sword2",
"temperedsword": "sword3",
"goldensword": "sword4",
"blueshield": "shield1",
"redshield": "shield2",
"mirrorshield": "shield3",
"bluemail": "mail2",
"redmail": "mail3",
"progressivearmor": "progressivemail",
"splus12": "halfmagic",
"splus14": "quartermagic",
"singlearrow": "arrow1",
"singlebomb": "bomb1",
"triforcepiece": "triforcepieces"
}
key = item.lower()
trans = {
" ": "",
'(': "",
'/': "",
')': "",
'+': "",
"magic": "",
"caneof": "",
"upgrade": "splus",
"arrows": "arrow",
"arrowplus": "arrowsplus",
"bombs": "bomb",
"bombplus": "bombsplus",
"rupees": "rupee"
}
for check in trans:
repl = trans[check]
key = key.replace(check,repl)
if key in label_switcher:
key = label_switcher.get(key)
return key
def generate_itempool(world, player):
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 not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
@@ -200,11 +254,10 @@ def generate_itempool(world, player):
world.get_location('Zelda Drop Off', player).event = True
world.get_location('Zelda Drop Off', player).locked = True
# 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[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray)
world.rupoor_cost = min(world.customitemarray[69], 9999)
world.rupoor_cost = min(world.customitemarray["rupoorcost"], 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[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.doorShuffle[player])
@@ -268,9 +321,9 @@ def generate_itempool(world, 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[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[30] == 0):
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray["heartcontainer"] == 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):
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray["heartpiece"] < 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
@@ -354,6 +407,7 @@ def set_up_take_anys(world, player):
world.initialize_regions()
def create_dynamic_shop_locations(world, player):
for shop in world.shops:
if shop.region.player == player:
@@ -389,7 +443,7 @@ 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, True)
fill_restrictive(world, all_state, prize_locs, prizepool, single_player_placement=True)
except FillError as e:
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1)
for location in empty_crystal_locations:
@@ -558,80 +612,31 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
placed_items[loc] = item
# Correct for insanely oversized item counts and take initial steps to handle undersized pools.
for x in range(0, 66):
if customitemarray[x] > total_items_to_place:
customitemarray[x] = total_items_to_place
if customitemarray[68] > total_items_to_place:
customitemarray[68] = total_items_to_place
itemtotal = 0
for x in range(0, 66):
itemtotal = itemtotal + customitemarray[x]
itemtotal = itemtotal + customitemarray[68]
itemtotal = itemtotal + customitemarray[70]
# Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors
for x in [*range(0, 66 + 1), 68, 69]:
key = CONST.CUSTOMITEMS[x]
if customitemarray[key] > total_items_to_place:
customitemarray[key] = total_items_to_place
pool.extend(['Bow'] * customitemarray[0])
pool.extend(['Silver Arrows']* customitemarray[1])
pool.extend(['Blue Boomerang'] * customitemarray[2])
pool.extend(['Red Boomerang'] * customitemarray[3])
pool.extend(['Hookshot'] * customitemarray[4])
pool.extend(['Mushroom'] * customitemarray[5])
pool.extend(['Magic Powder'] * customitemarray[6])
pool.extend(['Fire Rod'] * customitemarray[7])
pool.extend(['Ice Rod'] * customitemarray[8])
pool.extend(['Bombos'] * customitemarray[9])
pool.extend(['Ether'] * customitemarray[10])
pool.extend(['Quake'] * customitemarray[11])
pool.extend(['Lamp'] * customitemarray[12])
pool.extend(['Hammer'] * customitemarray[13])
pool.extend(['Shovel'] * customitemarray[14])
pool.extend(['Ocarina'] * customitemarray[15])
pool.extend(['Bug Catching Net'] * customitemarray[16])
pool.extend(['Book of Mudora'] * customitemarray[17])
pool.extend(['Cane of Somaria'] * customitemarray[19])
pool.extend(['Cane of Byrna'] * customitemarray[20])
pool.extend(['Cape'] * customitemarray[21])
pool.extend(['Pegasus Boots'] * customitemarray[23])
pool.extend(['Power Glove'] * customitemarray[24])
pool.extend(['Titans Mitts'] * customitemarray[25])
pool.extend(['Progressive Glove'] * customitemarray[26])
pool.extend(['Flippers'] * customitemarray[27])
pool.extend(['Piece of Heart'] * customitemarray[29])
pool.extend(['Boss Heart Container'] * customitemarray[30])
pool.extend(['Sanctuary Heart Container'] * customitemarray[31])
pool.extend(['Master Sword'] * customitemarray[33])
pool.extend(['Tempered Sword'] * customitemarray[34])
pool.extend(['Golden Sword'] * customitemarray[35])
pool.extend(['Blue Shield'] * customitemarray[37])
pool.extend(['Red Shield'] * customitemarray[38])
pool.extend(['Mirror Shield'] * customitemarray[39])
pool.extend(['Progressive Shield'] * customitemarray[40])
pool.extend(['Blue Mail'] * customitemarray[41])
pool.extend(['Red Mail'] * customitemarray[42])
pool.extend(['Progressive Armor'] * customitemarray[43])
pool.extend(['Magic Upgrade (1/2)'] * customitemarray[44])
pool.extend(['Magic Upgrade (1/4)'] * customitemarray[45])
pool.extend(['Bomb Upgrade (+5)'] * customitemarray[46])
pool.extend(['Bomb Upgrade (+10)'] * customitemarray[47])
pool.extend(['Arrow Upgrade (+5)'] * customitemarray[48])
pool.extend(['Arrow Upgrade (+10)'] * customitemarray[49])
pool.extend(['Single Arrow'] * customitemarray[50])
pool.extend(['Arrows (10)'] * customitemarray[51])
pool.extend(['Single Bomb'] * customitemarray[52])
pool.extend(['Bombs (3)'] * customitemarray[53])
pool.extend(['Rupee (1)'] * customitemarray[54])
pool.extend(['Rupees (5)'] * customitemarray[55])
pool.extend(['Rupees (20)'] * customitemarray[56])
pool.extend(['Rupees (50)'] * customitemarray[57])
pool.extend(['Rupees (100)'] * customitemarray[58])
pool.extend(['Rupees (300)'] * customitemarray[59])
pool.extend(['Rupoor'] * customitemarray[60])
pool.extend(['Blue Clock'] * customitemarray[61])
pool.extend(['Green Clock'] * customitemarray[62])
pool.extend(['Red Clock'] * customitemarray[63])
pool.extend(['Progressive Bow'] * customitemarray[64])
pool.extend(['Bombs (10)'] * customitemarray[65])
pool.extend(['Triforce Piece'] * customitemarray[66])
pool.extend(['Triforce'] * customitemarray[68])
# Triforce
if customitemarray["triforce"] > total_items_to_place:
customitemarray["triforce"] = total_items_to_place
itemtotal = 0
# Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors
for x in [*range(0, 66 + 1), 68, 69]:
key = CONST.CUSTOMITEMS[x]
itemtotal = itemtotal + customitemarray[key]
# Triforce
itemtotal = itemtotal + customitemarray["triforce"]
# Generic Keys
itemtotal = itemtotal + customitemarray["generickeys"]
customitems = [
"Bow", "Silver Arrows", "Blue Boomerang", "Red Boomerang", "Hookshot", "Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Catching Net", "Book of Mudora", "Cane of Somaria", "Cane of Byrna", "Cape", "Pegasus Boots", "Power Glove", "Titans Mitts", "Progressive Glove", "Flippers", "Piece of Heart", "Boss Heart Container", "Sanctuary Heart Container", "Master Sword", "Tempered Sword", "Golden Sword", "Blue Shield", "Red Shield", "Mirror Shield", "Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)", "Magic Upgrade (1/4)", "Bomb Upgrade (+5)", "Bomb Upgrade (+10)", "Arrow Upgrade (+5)", "Arrow Upgrade (+10)", "Single Arrow", "Arrows (10)", "Single Bomb", "Bombs (3)", "Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)", "Rupees (300)", "Rupoor", "Blue Clock", "Green Clock", "Red Clock", "Progressive Bow", "Bombs (10)", "Triforce Piece", "Triforce"
]
for customitem in customitems:
pool.extend([customitem] * customitemarray[get_custom_array_key(customitem)])
diff = difficulties[difficulty]
@@ -641,17 +646,17 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
# all bottles, since only one bottle is available
if diff.same_bottle:
thisbottle = random.choice(diff.bottles)
for _ in range(customitemarray[18]):
for _ in range(customitemarray["bottle"]):
if not diff.same_bottle:
thisbottle = random.choice(diff.bottles)
pool.append(thisbottle)
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.
if customitemarray["triforcepieces"] > 0 or customitemarray["triforcepiecesgoal"] > 0:
treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], 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[66] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[68] == 0):
extrapieces = treasure_hunt_count - customitemarray[66]
if (customitemarray["triforcepieces"] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray["triforce"] == 0):
extrapieces = treasure_hunt_count - customitemarray["triforcepieces"]
pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces
@@ -670,28 +675,30 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
if retro:
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
place_item(key_location, 'Small Key (Universal)')
pool.extend(['Small Key (Universal)'] * max((customitemarray[70] - 1), 0))
pool.extend(['Small Key (Universal)'] * max((customitemarray["generickeys"] - 1), 0))
else:
pool.extend(['Small Key (Universal)'] * customitemarray[70])
pool.extend(['Small Key (Universal)'] * customitemarray["generickeys"])
else:
pool.extend(['Small Key (Universal)'] * customitemarray[70])
pool.extend(['Small Key (Universal)'] * customitemarray["generickeys"])
pool.extend(['Fighter Sword'] * customitemarray[32])
pool.extend(['Progressive Sword'] * customitemarray[36])
pool.extend(['Fighter Sword'] * customitemarray["sword1"])
pool.extend(['Progressive Sword'] * customitemarray["progressivesword"])
if shuffle == 'insanity_legacy':
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))
pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0))
else:
pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28])
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
if retro:
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
nothings = total_items_to_place - itemtotal
print("Placing " + str(nothings) + " Nothings")
pool.extend(['Nothing'] * nothings)
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)

View File

@@ -147,7 +147,7 @@ def analyze_dungeon(key_layout, world, player):
key_layout.key_counters = create_key_counters(key_layout, world, player)
key_logic = key_layout.key_logic
find_bk_locked_sections(key_layout, world)
find_bk_locked_sections(key_layout, world, player)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
if world.retro[player] and world.mode[player] != 'standard':
return
@@ -304,14 +304,14 @@ def queue_sorter_2(queue_item):
return 1 if door.bigKey else 0
def find_bk_locked_sections(key_layout, world):
def find_bk_locked_sections(key_layout, world, player):
if key_layout.big_key_special:
return
key_counters = key_layout.key_counters
key_logic = key_layout.key_logic
bk_key_not_required = set()
big_chest_allowed_big_key = world.accessibility != 'locations'
big_chest_allowed_big_key = world.accessibility[player] != 'locations'
for counter in key_counters.values():
key_layout.all_chest_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.free_locations)
@@ -705,7 +705,7 @@ def unique_doors(doors):
def count_unique_sm_doors(doors):
unique_d_set = set()
for d in doors:
if d not in unique_d_set and d.dest not in unique_d_set and not d.bigKey:
if d not in unique_d_set and (d.dest not in unique_d_set or d.type == DoorType.SpiralStairs) and not d.bigKey:
unique_d_set.add(d)
return len(unique_d_set)
@@ -718,7 +718,8 @@ def count_unique_small_doors(key_counter, proposal):
if door in proposal and door not in counted:
cnt += 1
counted.add(door)
counted.add(door.dest)
if door.type != DoorType.SpiralStairs:
counted.add(door.dest)
return cnt
@@ -1069,15 +1070,6 @@ def invalid_self_locking_key(state, prev_state, prev_avail, world, player):
return prev_avail - 1 == 0
# does not allow dest doors
def count_unique_sm_doors(doors):
unique_d_set = set()
for d in doors:
if d not in unique_d_set and d.dest not in unique_d_set and not d.bigKey:
unique_d_set.add(d)
return len(unique_d_set)
def enough_small_locations(state, avail_small_loc):
unique_d_set = set()
for exp_door in state.small_doors:
@@ -1450,8 +1442,8 @@ def validate_key_placement(key_layout, world, player):
if not can_progress:
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key) or "- Boss" in l.name]
#missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
if len(missing_items) > 0: #world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
# missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
logging.getLogger('').error("Keylock - can't open locations: ")
for i in missing_locations:
logging.getLogger('').error(i)

14
Main.py
View File

@@ -24,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute
from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names
__version__ = '0.0.17pre'
__version__ = '0.0.18.5d'
def main(args, seed=None):
@@ -57,8 +57,8 @@ def main(args, seed=None):
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -185,9 +185,13 @@ def main(args, seed=None):
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)
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
if os.path.exists(args.enemizercli):
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
else:
logging.warning("EnemizerCLI not found at:" + args.enemizercli)
logging.warning("No Enemizer options will be applied until this is resolved.")
if args.race:
patch_race_rom(rom)

View File

@@ -148,6 +148,7 @@ def roll_settings(weights):
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
door_shuffle = get_choice('door_shuffle')
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.experimental = get_choice('experimental') == 'on'
goal = get_choice('goals')
ret.goal = {'ganon': 'ganon',
@@ -156,7 +157,7 @@ def roll_settings(weights):
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[goal]
ret.openpyramid = goal == 'fast_ganon'
ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
ret.crystals_gt = get_choice('tower_open')

View File

@@ -36,10 +36,6 @@ Doors are shuffled between dungeons as well.
Doors are not shuffled.
### Experimental
Used for development testing. This will be removed in a future version. Use at your own risk. Might play like a plando.
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.

16
Rom.py
View File

@@ -591,7 +591,7 @@ def patch_rom(world, rom, player, team, enemized):
patch_shuffled_dark_sanc(world, rom, player)
# patch doors
dr_flags = DROptions.Eternal_Mini_Bosses if not world.experimental[player] else DROptions.Town_Portal
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' or not world.experimental[player] else DROptions.Town_Portal
if world.doorShuffle[player] == 'crossed':
rom.write_byte(0x139004, 2)
rom.write_byte(0x151f1, 2)
@@ -899,7 +899,7 @@ def patch_rom(world, rom, player, team, enemized):
ERtimeincrease = 20
if world.keyshuffle[player] or world.bigkeyshuffle[player] or world.mapshuffle[player]:
ERtimeincrease = ERtimeincrease + 15
if world.clock_mode == 'off':
if world.clock_mode == 'none':
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
write_int32(rom, 0x180200, 0) # red clock adjustment time (in frames, sint32)
write_int32(rom, 0x180204, 0) # blue clock adjustment time (in frames, sint32)
@@ -981,8 +981,8 @@ def patch_rom(world, rom, player, team, enemized):
startingstate = CollectionState(world)
if startingstate.has('Bow', player):
equip[0x340] = 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro[player]:
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
@@ -1157,11 +1157,11 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if world.clock_mode != 'off':
if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player]:
rom.write_byte(0x18003C, 0x02) # show always
elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla':
elif world.dungeon_counters[player] == 'on':
rom.write_byte(0x18003C, 0x02) # always on
elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup
else:
rom.write_byte(0x18003C, 0x00)

View File

@@ -1185,14 +1185,13 @@ def set_inverted_big_bomb_rules(world, player):
'Hyrule Castle Entrance (East)',
'Inverted Ganons Tower',
'Cave 45',
'Checkerboard Cave']
'Checkerboard Cave',
'Inverted Big Bomb Shop']
LW_DM_entrances = ['Old Man Cave (East)',
'Old Man House (Bottom)',
'Old Man House (Top)',
'Death Mountain Return Cave (East)',
'Spectacle Rock Cave Peak',
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)',
'Tower of Hera',
'Death Mountain Return Cave (West)',
'Paradox Cave (Top)',
@@ -1212,7 +1211,7 @@ def set_inverted_big_bomb_rules(world, player):
'Chest Game',
'Dark World Hammer Peg Cave',
'Red Shield Shop',
'Dark Sanctuary Hint',
'Inverted Dark Sanctuary',
'Fortune Teller (Dark)',
'Dark World Shop',
'Dark World Lumberjack Shop',
@@ -1222,7 +1221,7 @@ def set_inverted_big_bomb_rules(world, player):
Southern_DW_entrances = ['Hype Cave',
'Bonk Fairy (Dark)',
'Archery Game',
'Inverted Big Bomb Shop',
'Inverted Links House',
'Dark Lake Hylia Shop',
'Swamp Palace']
Isolated_DW_entrances = ['Spike Cave',

View File

@@ -3,6 +3,7 @@ import os
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
def int16_as_bytes(value):
value = value & 0xFFFF
@@ -34,6 +35,8 @@ def is_bundled():
return getattr(sys, 'frozen', False)
def local_path(path):
return path
if local_path.cached_path is not None:
return os.path.join(local_path.cached_path, path)
@@ -239,6 +242,46 @@ def print_wiki_doors(d_regions, world, player):
print('|}')
def print_xml_doors(d_regions, world, player):
root = ET.Element('root')
for d, region_list in d_regions.items():
tile_map = {}
for region in region_list:
tile = None
r = world.get_region(region, player)
for ext in r.exits:
door = world.check_for_door(ext.name, player)
if door is not None and door.roomIndex != -1:
tile = door.roomIndex
break
if tile is not None:
if tile not in tile_map:
tile_map[tile] = []
tile_map[tile].append(r)
dungeon = ET.SubElement(root, 'dungeon', {'name': d})
for tile, r_list in tile_map.items():
supertile = ET.SubElement(dungeon, 'supertile', {'id': str(tile)})
for region in r_list:
room = ET.SubElement(supertile, 'room', {'name': region.name})
for ext in region.exits:
ET.SubElement(room, 'door', {'name': ext.name})
ET.dump(root)
def print_graph(world):
root = ET.Element('root')
for region in world.regions:
r = ET.SubElement(root, 'region', {'name': region.name})
for ext in region.exits:
attribs = {'name': ext.name}
if ext.connected_region:
attribs['connected_region'] = ext.connected_region.name
if ext.door and ext.door.dest:
attribs['dest'] = ext.door.dest.name
ET.SubElement(r, 'exit', attribs)
ET.dump(root)
if __name__ == '__main__':
pass
# make_new_base2current()

View File

@@ -19,8 +19,8 @@ incsrc spiral.asm
incsrc gfx.asm
incsrc keydoors.asm
incsrc overrides.asm
incsrc edges.asm
incsrc math.asm
;incsrc edges.asm
;incsrc math.asm
warnpc $279000
; Data Section

View File

@@ -76,7 +76,8 @@ LoadRoomHorz:
sty $06 : sta $07 : lda $a0 : pha ; Store normal room on stack
lda $07 : jsr LookupNewRoom ; New room is in A, Room Data is in $00
lda $01 : and.b #$80 : cmp #$80 : bne .gtg
jsr HorzEdge : pla : bcs .end
; jsr HorzEdge : pla : bcs .end
pla
sta $a0 : bra .end ; Restore normal room, abort (straight staircases and open edges can get in this routine)
.gtg ;Good to Go!
@@ -106,7 +107,8 @@ LoadRoomVert:
sty $06 : sta $07 : lda $a0 : pha ; Store normal room on stack
lda $07 : jsr LookupNewRoom ; New room is in A, Room Data is in $00
lda $01 : and.b #$80 : cmp #$80 : bne .gtg
jsr VertEdge : pla : bcs .end
; jsr VertEdge : pla : bcs .end
pla
sta $a0 : bra .end ; Restore normal room, abort (straight staircases and open edges can get in this routine)
.gtg ;Good to Go!
pla ; Throw away normal room (don't fill up the stack)

21
build-dr.py Normal file
View File

@@ -0,0 +1,21 @@
import subprocess
import os
import shutil
DEST_DIRECTORY = '.'
if os.path.isdir("upx"):
upx_string = "--upx-dir=upx"
else:
upx_string = ""
if os.path.isdir("build"):
shutil.rmtree("build")
subprocess.run(" ".join(["pyinstaller DungeonRandomizer.spec ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)

21
build-gui.py Normal file
View File

@@ -0,0 +1,21 @@
import subprocess
import os
import shutil
DEST_DIRECTORY = '.'
if os.path.isdir("upx"):
upx_string = "--upx-dir=upx"
else:
upx_string = ""
if os.path.isdir("build"):
shutil.rmtree("build")
subprocess.run(" ".join(["pyinstaller Gui.spec ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)

347
classes/SpriteSelector.py Normal file
View File

@@ -0,0 +1,347 @@
from tkinter import filedialog, messagebox, Button, Canvas, Label, LabelFrame, Frame, PhotoImage, Scrollbar, Toplevel, ALL, NSEW, LEFT, BOTTOM, X, RIGHT, TOP, HORIZONTAL, EW, NS
from glob import glob
import json
import os
import random
import shutil
from urllib.parse import urlparse
from urllib.request import urlopen
import webbrowser
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from Rom import Sprite
from Utils import is_bundled, local_path, output_path, open_file
class SpriteSelector(object):
def __init__(self, parent, callback, adjuster=False):
if is_bundled():
self.deploy_icons()
self.parent = parent
self.window = Toplevel(parent)
self.window.geometry("800x650")
self.sections = []
self.callback = callback
self.adjuster = adjuster
self.window.wm_title("TAKE ANY ONE YOU WANT")
self.window['padx'] = 5
self.window['pady'] = 5
self.all_sprites = []
def open_official_sprite_listing(_evt):
webbrowser.open("http://alttpr.com/sprite_preview")
def open_unofficial_sprite_dir(_evt):
open_file(self.unofficial_sprite_dir)
def open_spritesomething_listing(_evt):
webbrowser.open("https://artheau.github.io/SpriteSomething/?mode=zelda3/link")
official_frametitle = Frame(self.window)
official_title_text = Label(official_frametitle, text="Official Sprites")
official_title_link = Label(official_frametitle, text="(open)", fg="blue", cursor="hand2")
official_title_text.pack(side=LEFT)
official_title_link.pack(side=LEFT)
official_title_link.bind("<Button-1>", open_official_sprite_listing)
unofficial_frametitle = Frame(self.window)
unofficial_title_text = Label(unofficial_frametitle, text="Unofficial Sprites")
unofficial_title_link = Label(unofficial_frametitle, text="(open)", fg="blue", cursor="hand2")
unofficial_title_text.pack(side=LEFT)
unofficial_title_link.pack(side=LEFT)
unofficial_title_link.bind("<Button-1>", open_unofficial_sprite_dir)
spritesomething_title_link = Label(unofficial_frametitle, text="(SpriteSomething)", fg="blue", cursor="hand2")
spritesomething_title_link.pack(side=LEFT)
spritesomething_title_link.bind("<Button-1>", open_spritesomething_listing)
self.icon_section(official_frametitle, self.official_sprite_dir+'/*', 'Official sprites not found. Click "Update official sprites" to download them.')
self.icon_section(unofficial_frametitle, self.unofficial_sprite_dir+'/*', 'Put sprites in the unofficial sprites folder (see open link above) to have them appear here.')
frame = Frame(self.window)
frame.pack(side=BOTTOM, fill=X, pady=5)
button = Button(frame, text="Browse for file...", command=self.browse_for_sprite)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Update official sprites", command=self.update_official_sprites)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite)
button.pack(side=LEFT, padx=(0, 5))
button = Button(frame, text="Random sprite", command=self.use_random_sprite)
button.pack(side=LEFT, padx=(0, 5))
if adjuster:
button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite)
button.pack(side=LEFT, padx=(0, 5))
set_icon(self.window)
self.window.focus()
def icon_section(self, frame_label, path, no_results_label):
frame = LabelFrame(self.window, labelwidget=frame_label, padx=5, pady=5)
canvas = Canvas(frame, borderwidth=0)
y_scrollbar = Scrollbar(frame, orient="vertical", command=canvas.yview)
y_scrollbar.pack(side="right", fill="y")
content_frame = Frame(canvas)
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4, 4), window=content_frame, anchor="nw")
canvas.configure(yscrollcommand=y_scrollbar.set)
def onFrameConfigure(canvas):
"""Reset the scroll region to encompass the inner frame"""
canvas.configure(scrollregion=canvas.bbox("all"))
content_frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
frame.pack(side=TOP, fill=X)
sprites = []
for file in glob(output_path(path)):
sprites.append(Sprite(file))
sprites.sort(key=lambda s: str.lower(s.name or "").strip())
i = 0
for sprite in sprites:
image = get_image_for_sprite(sprite)
if image is None:
continue
self.all_sprites.append(sprite)
button = Button(content_frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
ToolTips.register(button, sprite.name + ("\nBy: %s" % sprite.author_name if sprite.author_name else ""))
button.image = image
button.grid(row=i // 16, column=i % 16)
i += 1
if i == 0:
label = Label(content_frame, text=no_results_label)
label.pack()
def update_official_sprites(self):
# need to wrap in try catch. We don't want errors getting the json or downloading the files to break us.
self.window.destroy()
self.parent.update()
def work(task):
resultmessage = ""
successful = True
def finished():
task.close_window()
if successful:
messagebox.showinfo("Sprite Updater", resultmessage)
else:
messagebox.showerror("Sprite Updater", resultmessage)
SpriteSelector(self.parent, self.callback, self.adjuster)
try:
task.update_status("Downloading official sprites list")
with urlopen('https://alttpr.com/sprites') as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of official sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
task.queue_event(finished)
return
try:
task.update_status("Determining needed sprites")
current_sprites = [os.path.basename(file) for file in glob(self.official_sprite_dir+'/*')]
official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites]
bundled_sprites = [os.path.basename(file) for file in glob(self.local_official_sprite_dir+'/*')]
# todo: eventually use the above list to avoid downloading any sprites that we already have cached in the bundle.
official_filenames = [filename for (_, filename) in official_sprites]
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in official_filenames]
except Exception as e:
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
task.queue_event(finished)
return
updated = 0
for (sprite_url, filename) in needed_sprites:
try:
task.update_status("Downloading needed sprite %g/%g" % (updated + 1, len(needed_sprites)))
target = os.path.join(self.official_sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
shutil.copyfileobj(response, out)
except Exception as e:
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
updated += 1
deleted = 0
for sprite in obsolete_sprites:
try:
task.update_status("Removing obsolete sprite %g/%g" % (deleted + 1, len(obsolete_sprites)))
os.remove(os.path.join(self.official_sprite_dir, sprite))
except Exception as e:
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
deleted += 1
if successful:
resultmessage = "official sprites updated successfully"
task.queue_event(finished)
BackgroundTaskProgress(self.parent, work, "Updating Sprites")
def browse_for_sprite(self):
sprite = filedialog.askopenfilename(
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
("ZSprite files", ".zspr"),
("Sprite files", ".spr"),
("Rom Files", (".sfc", ".smc")),
("All Files", "*")])
try:
self.callback(Sprite(sprite))
except Exception:
self.callback(None)
self.window.destroy()
def use_default_sprite(self):
self.callback(None, False)
self.window.destroy()
def use_default_link_sprite(self):
self.callback(Sprite.default_link_sprite(), False)
self.window.destroy()
def use_random_sprite(self):
self.callback(random.choice(self.all_sprites) if self.all_sprites else None, True)
self.window.destroy()
def select_sprite(self, spritename):
self.callback(spritename, False)
self.window.destroy()
def deploy_icons(self):
if not os.path.exists(self.unofficial_sprite_dir):
os.makedirs(self.unofficial_sprite_dir)
if not os.path.exists(self.official_sprite_dir):
shutil.copytree(self.local_official_sprite_dir, self.official_sprite_dir)
@property
def official_sprite_dir(self):
if is_bundled():
return output_path("sprites/official")
return self.local_official_sprite_dir
@property
def local_official_sprite_dir(self):
return local_path("data/sprites/official")
@property
def unofficial_sprite_dir(self):
if is_bundled():
return output_path("sprites/unofficial")
return self.local_unofficial_sprite_dir
@property
def local_unofficial_sprite_dir(self):
return local_path("data/sprites/unofficial")
def get_image_for_sprite(sprite):
if not sprite.valid:
return None
height = 24
width = 16
def draw_sprite_into_gif(add_palette_color, set_pixel_color_index):
def drawsprite(spr, pal_as_colors, offset):
for y, row in enumerate(spr):
for x, pal_index in enumerate(row):
if pal_index:
color = pal_as_colors[pal_index - 1]
set_pixel_color_index(x + offset[0], y + offset[1], color)
add_palette_color(16, (40, 40, 40))
shadow = [
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
]
drawsprite(shadow, [16], (2, 17))
palettes = sprite.decode_palette()
for i in range(15):
add_palette_color(i + 1, palettes[0][i])
body = sprite.decode16(0x4C0)
drawsprite(body, list(range(1, 16)), (0, 8))
head = sprite.decode16(0x40)
drawsprite(head, list(range(1, 16)), (0, 0))
def make_gif(callback):
gif_header = b'GIF89a'
gif_lsd = bytearray(7)
gif_lsd[0] = width
gif_lsd[2] = height
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[5] = 0 # background color is zero
gif_lsd[6] = 0 # aspect raio not specified
gif_gct = bytearray(3 * 32)
gif_gce = bytearray(8)
gif_gce[0] = 0x21 # start of extention blocked
gif_gce[1] = 0xF9 # identifies this as the Graphics Control extension
gif_gce[2] = 4 # we are suppling only the 4 four bytes
gif_gce[3] = 0x01 # this gif includes transparency
gif_gce[4] = gif_gce[5] = 0 # animation frrame delay (unused)
gif_gce[6] = 0 # transparent color is index 0
gif_gce[7] = 0 # end of gif_gce
gif_id = bytearray(10)
gif_id[0] = 0x2c
# byte 1,2 are image left. 3,4 are image top both are left as zerosuitsamus
gif_id[5] = width
gif_id[7] = height
gif_id[9] = 0 # no local color table
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
clear = 0x80
stop = 0x81
unchunked_image_data = bytearray(height * (width + 1) + 1)
# we technically need a Clear code once every 125 bytes, but we do it at the start of every row for simplicity
for row in range(height):
unchunked_image_data[row * (width + 1)] = clear
unchunked_image_data[-1] = stop
def add_palette_color(index, color):
gif_gct[3 * index] = color[0]
gif_gct[3 * index + 1] = color[1]
gif_gct[3 * index + 2] = color[2]
def set_pixel_color_index(x, y, color):
unchunked_image_data[y * (width + 1) + x + 1] = color
callback(add_palette_color, set_pixel_color_index)
def chunk_image(img):
for i in range(0, len(img), 255):
chunk = img[i:i + 255]
yield bytes([len(chunk)])
yield chunk
gif_img = b''.join([gif_img_minimum_code_size] + list(chunk_image(unchunked_image_data)) + [b'\x00'])
gif = b''.join([gif_header, gif_lsd, gif_gct, gif_gce, gif_id, gif_img, b'\x3b'])
return gif
gif_data = make_gif(draw_sprite_into_gif)
image = PhotoImage(data=gif_data)
return image.zoom(2)

1
classes/__init__.py Normal file
View File

@@ -0,0 +1 @@
# do nothing, just exist to make "classes" package

109
classes/constants.py Normal file
View File

@@ -0,0 +1,109 @@
CUSTOMITEMS = [
"bow", "progressivebow", "boomerang", "redmerang", "hookshot",
"mushroom", "powder", "firerod", "icerod", "bombos",
"ether", "quake", "lamp", "hammer", "shovel",
"flute", "bugnet", "book", "bottle", "somaria",
"byrna", "cape", "mirror", "boots", "powerglove",
"titansmitt", "progressiveglove", "flippers", "pearl", "heartpiece",
"heartcontainer", "sancheart", "sword1", "sword2", "sword3",
"sword4", "progressivesword", "shield1", "shield2", "shield3",
"progressiveshield", "mail2", "mail3", "progressivemail", "halfmagic",
"quartermagic", "bombsplus5", "bombsplus10", "arrowsplus5", "arrowsplus10",
"arrow1", "arrow10", "bomb1", "bomb3", "bomb10",
"rupee1", "rupee5", "rupee20", "rupee50", "rupee100",
"rupee300", "blueclock", "greenclock", "redclock", "silversupgrade",
"generickeys", "triforcepieces", "triforcepiecesgoal", "triforce", "rupoor",
"rupoorcost"
]
CANTSTARTWITH = [
"triforcepiecesgoal", "triforce", "rupoor",
"rupoorcost"
]
CUSTOMITEMLABELS = [
"Bow", "Progressive Bow", "Blue Boomerang", "Red Boomerang", "Hookshot",
"Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos",
"Ether", "Quake", "Lamp", "Hammer", "Shovel",
"Ocarina", "Bug Catching Net", "Book of Mudora", "Bottle", "Cane of Somaria",
"Cane of Byrna", "Magic Cape", "Magic Mirror", "Pegasus Boots", "Power Glove",
"Titans Mitts", "Progressive Glove", "Flippers", "Moon Pearl", "Piece of Heart",
"Boss Heart Container", "Sanctuary Heart Container", "Fighter Sword", "Master Sword", "Tempered Sword",
"Golden Sword", "Progressive Sword", "Blue Shield", "Red Shield", "Mirror Shield",
"Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)",
"Magic Upgrade (1/4)", "Bomb Upgrade (+5)", "Bomb Upgrade (+10)", "Arrow Upgrade (+5)", "Arrow Upgrade (+10)",
"Single Arrow", "Arrows (10)", "Single Bomb", "Bombs (3)", "Bombs (10)",
"Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)",
"Rupees (300)", "Blue Clock", "Green Clock", "Red Clock", "Silver Arrows",
"Small Key (Universal)", "Triforce Piece", "Triforce Piece Goal", "Triforce", "Rupoor",
"Rupoor Cost"
]
SETTINGSTOPROCESS = {
"randomizer": {
"item": {
"retro": "retro",
"worldstate": "mode",
"logiclevel": "logic",
"goal": "goal",
"crystals_gt": "crystals_gt",
"crystals_ganon": "crystals_ganon",
"weapons": "swords",
"itempool": "difficulty",
"itemfunction": "item_functionality",
"timer": "timer",
"progressives": "progressive",
"accessibility": "accessibility",
"sortingalgo": "algorithm"
},
"entrance": {
"openpyramid": "openpyramid",
"shuffleganon": "shuffleganon",
"entranceshuffle": "shuffle"
},
"enemizer": {
"potshuffle": "shufflepots",
"enemyshuffle": "shuffleenemies",
"bossshuffle": "shufflebosses",
"enemydamage": "enemy_damage",
"enemyhealth": "enemy_health"
},
"dungeon": {
"mapshuffle": "mapshuffle",
"compassshuffle": "compassshuffle",
"smallkeyshuffle": "keyshuffle",
"bigkeyshuffle": "bigkeyshuffle",
"dungeondoorshuffle": "door_shuffle",
"experimental": "experimental",
"dungeon_counters": "dungeon_counters"
},
"multiworld": {
"names": "names"
},
"gameoptions": {
"hints": "hints",
"nobgm": "disablemusic",
"quickswap": "quickswap",
"heartcolor": "heartcolor",
"heartbeep": "heartbeep",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes"
},
"generation": {
"spoiler": "create_spoiler",
"suppressrom": "suppress_rom",
"usestartinventory": "usestartinventory",
"usecustompool": "custom",
"saveonexit": "saveonexit"
}
}
}

1
gui/__init__.py Normal file
View File

@@ -0,0 +1 @@
# do nothing, just exist to make "gui" package

1
gui/about/__init__.py Normal file
View File

@@ -0,0 +1 @@
# do nothing, just exist to make "gui.about" package

1
gui/adjust/__init__.py Normal file
View File

@@ -0,0 +1 @@
# do nothing, just exist to make "gui.adjust" package

113
gui/adjust/overview.py Normal file
View File

@@ -0,0 +1,113 @@
from tkinter import ttk, filedialog, messagebox, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, OptionMenu, E, W, LEFT, RIGHT, X, BOTTOM
from AdjusterMain import adjust
from argparse import Namespace
from classes.SpriteSelector import SpriteSelector
import gui.widgets as widgets
import json
import logging
import os
def adjust_page(top, parent, settings):
# Adjust page
self = ttk.Frame(parent)
# Adjust options
self.widgets = {}
# Adjust option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["selectOptionsFrame"] = Frame(self)
self.frames["leftAdjustFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["rightAdjustFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["bottomAdjustFrame"] = Frame(self)
self.frames["selectOptionsFrame"].pack(fill=X)
self.frames["leftAdjustFrame"].pack(side=LEFT)
self.frames["rightAdjustFrame"].pack(side=RIGHT)
self.frames["bottomAdjustFrame"].pack(fill=X)
with open(os.path.join("resources","app","gui","adjust","overview","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
# Sprite Selection
self.spriteNameVar2 = StringVar()
spriteDialogFrame2 = Frame(self.frames["leftAdjustFrame"])
baseSpriteLabel2 = Label(spriteDialogFrame2, text='Sprite:')
spriteEntry2 = Label(spriteDialogFrame2, textvariable=self.spriteNameVar2)
self.sprite = None
def set_sprite(sprite_param, random_sprite=False):
if sprite_param is None or not sprite_param.valid:
self.sprite = None
self.spriteNameVar2.set('(unchanged)')
else:
self.sprite = sprite_param
self.spriteNameVar2.set(self.sprite.name)
top.randomSprite.set(random_sprite)
def SpriteSelectAdjuster():
SpriteSelector(parent, set_sprite, adjuster=True)
spriteSelectButton2 = Button(spriteDialogFrame2, text='...', command=SpriteSelectAdjuster)
baseSpriteLabel2.pack(side=LEFT)
spriteEntry2.pack(side=LEFT)
spriteSelectButton2.pack(side=LEFT)
spriteDialogFrame2.pack(anchor=E)
adjustRomFrame = Frame(self.frames["bottomAdjustFrame"])
adjustRomLabel = Label(adjustRomFrame, text='Rom to adjust: ')
self.romVar2 = StringVar(value=settings["rom"])
romEntry2 = Entry(adjustRomFrame, textvariable=self.romVar2)
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
if rom:
settings["rom"] = rom
self.romVar2.set(rom)
romSelectButton2 = Button(adjustRomFrame, text='Select Rom', command=RomSelect2)
adjustRomLabel.pack(side=LEFT)
romEntry2.pack(side=LEFT, fill=X, expand=True)
romSelectButton2.pack(side=LEFT)
adjustRomFrame.pack(fill=X)
def adjustRom():
options = {
"heartbeep": "heartbeep",
"heartcolor": "heartcolor",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes",
"quickswap": "quickswap",
"nobgm": "disablemusic"
}
guiargs = Namespace()
for option in options:
arg = options[option]
setattr(guiargs, arg, self.widgets[option].storageVar.get())
guiargs.rom = self.romVar2.get()
guiargs.baserom = top.pages["randomizer"].pages["generation"].romVar.get()
guiargs.sprite = self.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")
adjustButton = Button(self.frames["bottomAdjustFrame"], text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=BOTTOM, padx=(5, 0))
return self,settings

147
gui/bottom.py Normal file
View File

@@ -0,0 +1,147 @@
from tkinter import ttk, messagebox, StringVar, Button, Entry, Frame, Label, Spinbox, E, W, LEFT, RIGHT, X
from argparse import Namespace
from functools import partial
import logging
import os
import random
from CLI import parse_arguments, get_settings
from Main import main
from Utils import local_path, output_path, open_file
import classes.constants as CONST
import gui.widgets as widgets
def bottom_frame(self, parent, args=None):
# Bottom Frame
self = ttk.Frame(parent)
# Bottom Frame options
self.widgets = {}
seedCountFrame = Frame(self)
seedCountFrame.pack()
## Seed #
seedLabel = Label(self, text='Seed #')
savedSeed = parent.settings["seed"]
self.seedVar = StringVar(value=savedSeed)
def saveSeed(caller,_,mode):
savedSeed = self.seedVar.get()
parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None
self.seedVar.trace_add("write",saveSeed)
seedEntry = Entry(self, width=15, textvariable=self.seedVar)
seedLabel.pack(side=LEFT)
seedEntry.pack(side=LEFT)
## Number of Generation attempts
key = "generationcount"
self.widgets[key] = widgets.make_widget(
self,
"spinbox",
self,
"Count",
None,
None,
{"label": {"side": LEFT}, "spinbox": {"side": RIGHT}}
)
self.widgets[key].pack(side=LEFT)
def generateRom():
guiargs = create_guiargs(parent)
# 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 guiargs.count is not None:
seed = guiargs.seed
for _ in range(guiargs.count):
main(seed=seed, args=guiargs)
seed = random.randint(0, 999999999)
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")
## Generate Button
generateButton = Button(self, text='Generate Patched Rom', command=generateRom)
generateButton.pack(side=LEFT)
def open_output():
if args and args.outputpath:
open_file(output_path(args.outputpath))
else:
open_file(output_path(parent.settings["outputpath"]))
openOutputButton = Button(self, text='Open Output Directory', command=open_output)
openOutputButton.pack(side=RIGHT)
## Documentation Button
if os.path.exists(local_path('README.html')):
def open_readme():
open_file(local_path('README.html'))
openReadmeButton = Button(self, text='Open Documentation', command=open_readme)
openReadmeButton.pack(side=RIGHT)
return self
def create_guiargs(parent):
guiargs = Namespace()
# set up settings to gather
# Page::Subpage::GUI-id::param-id
options = CONST.SETTINGSTOPROCESS
for mainpage in options:
for subpage in options[mainpage]:
for widget in options[mainpage][subpage]:
arg = options[mainpage][subpage][widget]
setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get())
guiargs.enemizercli = parent.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.get()
guiargs.multi = int(parent.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.get())
guiargs.rom = parent.pages["randomizer"].pages["generation"].romVar.get()
guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get())
guiargs.seed = int(parent.frames["bottom"].seedVar.get()) if parent.frames["bottom"].seedVar.get() else None
guiargs.count = int(parent.frames["bottom"].widgets["generationcount"].storageVar.get()) if parent.frames["bottom"].widgets["generationcount"].storageVar.get() != '1' else None
adjustargs = {
"nobgm": "disablemusic",
"quickswap": "quickswap",
"heartcolor": "heartcolor",
"heartbeep": "heartbeep",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes"
}
for adjustarg in adjustargs:
internal = adjustargs[adjustarg]
setattr(guiargs,"adjust." + internal, parent.pages["adjust"].content.widgets[adjustarg].storageVar.get())
customitems = CONST.CUSTOMITEMS
guiargs.startinventory = []
guiargs.customitemarray = {}
guiargs.startinventoryarray = {}
for customitem in customitems:
if customitem not in ["triforcepiecesgoal", "triforce", "rupoor", "rupoorcost"]:
amount = int(parent.pages["startinventory"].content.startingWidgets[customitem].storageVar.get())
guiargs.startinventoryarray[customitem] = amount
for i in range(0, amount):
label = CONST.CUSTOMITEMLABELS[customitems.index(customitem)]
guiargs.startinventory.append(label)
guiargs.customitemarray[customitem] = int(parent.pages["custom"].content.customWidgets[customitem].storageVar.get())
guiargs.startinventory = ','.join(guiargs.startinventory)
guiargs.sprite = parent.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"]
guiargs.randomSprite = parent.randomSprite.get()
guiargs.outputpath = parent.outputPath.get()
return guiargs

1
gui/custom/__init__.py Normal file
View File

@@ -0,0 +1 @@
# do nothing, just exist to make "gui.custom" package

55
gui/custom/overview.py Normal file
View File

@@ -0,0 +1,55 @@
from tkinter import ttk, Frame, N, LEFT, VERTICAL, Y
import gui.widgets as widgets
import json
import os
import classes.constants as CONST
def custom_page(top, parent):
# Custom Item Pool
self = ttk.Frame(parent)
def create_list_frame(parent, framename):
parent.frames[framename] = Frame(parent)
parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N)
parent.frames[framename].thisRow = 0
parent.frames[framename].thisCol = 0
def create_vertical_rule(num=1):
for i in range(0,num):
ttk.Separator(self, orient=VERTICAL).pack(side=LEFT, anchor=N, fill=Y)
def validation(P):
if str.isdigit(P) or P == "":
return True
else:
return False
vcmd=(self.register(validation), '%P')
# Custom Item Pool options
self.customWidgets = {}
# Custom Item Pool option sections
self.frames = {}
create_list_frame(self, "itemList1")
create_vertical_rule(2)
create_list_frame(self, "itemList2")
create_vertical_rule(2)
create_list_frame(self, "itemList3")
create_vertical_rule(2)
create_list_frame(self, "itemList4")
create_vertical_rule(2)
create_list_frame(self, "itemList5")
with open(os.path.join("resources", "app", "gui", "custom", "overview", "widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.customWidgets[key] = dictWidgets[key]
for key in CONST.CUSTOMITEMS:
self.customWidgets[key].storageVar.set(top.settings["customitemarray"][key])
return self

74
gui/loadcliargs.py Normal file
View File

@@ -0,0 +1,74 @@
from classes.SpriteSelector import SpriteSelector as spriteSelector
from gui.randomize.gameoptions import set_sprite
from Rom import Sprite, get_sprite_from_name
import classes.constants as CONST
def loadcliargs(gui, args, settings=None):
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
# set up options to get
# Page::Subpage::GUI-id::param-id
options = CONST.SETTINGSTOPROCESS
for mainpage in options:
for subpage in options[mainpage]:
for widget in options[mainpage][subpage]:
arg = options[mainpage][subpage][widget]
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg])
if subpage == "gameoptions" and not widget == "hints":
hasSettings = settings is not None
hasWidget = ("adjust." + widget) in settings if hasSettings else None
if hasWidget is None:
gui.pages["adjust"].content.widgets[widget].storageVar.set(args[arg])
gui.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.set(args["enemizercli"])
gui.pages["randomizer"].pages["generation"].romVar.set(args["rom"])
if args["multi"]:
gui.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.set(str(args["multi"]))
if args["seed"]:
gui.frames["bottom"].seedVar.set(str(args["seed"]))
if args["count"]:
gui.frames["bottom"].widgets["generationcount"].storageVar.set(str(args["count"]))
gui.outputPath.set(args["outputpath"])
def sprite_setter(spriteObject):
gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"] = spriteObject
if args["sprite"] is not None:
sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"])
set_sprite(sprite_obj, False, spriteSetter=sprite_setter,
spriteNameVar=gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteNameVar"],
randomSpriteVar=gui.randomSprite)
def sprite_setter_adj(spriteObject):
gui.pages["adjust"].content.sprite = spriteObject
if args["sprite"] is not None:
sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"])
set_sprite(sprite_obj, False, spriteSetter=sprite_setter_adj,
spriteNameVar=gui.pages["adjust"].content.spriteNameVar2,
randomSpriteVar=gui.randomSprite)
def loadadjustargs(gui, settings):
options = {
"adjust": {
"content": {
"nobgm": "adjust.nobgm",
"quickswap": "adjust.quickswap",
"heartcolor": "adjust.heartcolor",
"heartbeep": "adjust.heartbeep",
"menuspeed": "adjust.menuspeed",
"owpalettes": "adjust.owpalettes",
"uwpalettes": "adjust.uwpalettes"
}
}
}
for mainpage in options:
for subpage in options[mainpage]:
for widget in options[mainpage][subpage]:
key = options[mainpage][subpage][widget]
if key in settings:
gui.pages[mainpage].content.widgets[widget].storageVar.set(settings[key])

View File

@@ -0,0 +1 @@
# do nothing, just exist to make "gui.randomize" package

38
gui/randomize/dungeon.py Normal file
View File

@@ -0,0 +1,38 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def dungeon_page(parent):
# Dungeon Shuffle
self = ttk.Frame(parent)
# Dungeon Shuffle options
self.widgets = {}
# Dungeon Shuffle option sections
self.frames = {}
self.frames["keysanity"] = Frame(self)
self.frames["keysanity"].pack(anchor=W)
## Dungeon Item Shuffle
mscbLabel = Label(self.frames["keysanity"], text="Shuffle: ")
mscbLabel.pack(side=LEFT)
with open(os.path.join("resources","app","gui","randomize","dungeon","keysanity.json")) as keysanityItems:
myDict = json.load(keysanityItems)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(side=LEFT)
self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","dungeon","widgets.json")) as dungeonWidgets:
myDict = json.load(dungeonWidgets)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(anchor=W)
return self

63
gui/randomize/enemizer.py Normal file
View File

@@ -0,0 +1,63 @@
import os
from tkinter import ttk, filedialog, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, LabelFrame, OptionMenu, N, E, W, LEFT, RIGHT, BOTTOM, X
import gui.widgets as widgets
import json
import os
import webbrowser
def enemizer_page(parent,settings):
def open_enemizer_download(_evt):
webbrowser.open("https://github.com/Bonta0/Enemizer/releases")
# Enemizer
self = ttk.Frame(parent)
# Enemizer options
self.widgets = {}
# Enemizer option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["selectOptionsFrame"] = Frame(self)
self.frames["leftEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["rightEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["bottomEnemizerFrame"] = Frame(self)
self.frames["selectOptionsFrame"].pack(fill=X)
self.frames["leftEnemizerFrame"].pack(side=LEFT)
self.frames["rightEnemizerFrame"].pack(side=RIGHT)
self.frames["bottomEnemizerFrame"].pack(fill=X)
with open(os.path.join("resources","app","gui","randomize","enemizer","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
## Enemizer CLI Path
enemizerPathFrame = Frame(self.frames["bottomEnemizerFrame"])
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
enemizerCLIlabel.pack(side=LEFT)
enemizerURL = Label(enemizerPathFrame, text="(get online)", fg="blue", cursor="hand2")
enemizerURL.pack(side=LEFT)
enemizerURL.bind("<Button-1>", open_enemizer_download)
self.enemizerCLIpathVar = StringVar(value=settings["enemizercli"])
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=self.enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT, fill=X, expand=True)
def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join("."))
if path:
self.enemizerCLIpathVar.set(path)
settings["enemizercli"] = path
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT)
enemizerPathFrame.pack(fill=X)
return self,settings

29
gui/randomize/entrando.py Normal file
View File

@@ -0,0 +1,29 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def entrando_page(parent):
# Entrance Randomizer
self = ttk.Frame(parent)
# Entrance Randomizer options
self.widgets = {}
# Entrance Randomizer option sections
self.frames = {}
self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","entrando","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
return self

View File

@@ -0,0 +1,78 @@
from tkinter import ttk, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
from functools import partial
import classes.SpriteSelector as spriteSelector
import gui.widgets as widgets
import json
import os
def gameoptions_page(top, parent):
# Game Options
self = ttk.Frame(parent)
# Game Options options
self.widgets = {}
# Game Options option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["leftRomOptionsFrame"] = Frame(self)
self.frames["rightRomOptionsFrame"] = Frame(self)
self.frames["leftRomOptionsFrame"].pack(side=LEFT)
self.frames["rightRomOptionsFrame"].pack(side=RIGHT)
with open(os.path.join("resources","app","gui","randomize","gameoptions","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
## Sprite selection
spriteDialogFrame = Frame(self.frames["leftRomOptionsFrame"])
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
self.widgets["sprite"] = {}
self.widgets["sprite"]["spriteObject"] = None
self.widgets["sprite"]["spriteNameVar"] = StringVar()
self.widgets["sprite"]["spriteNameVar"].set('(unchanged)')
spriteEntry = Label(spriteDialogFrame, textvariable=self.widgets["sprite"]["spriteNameVar"])
def sprite_setter(spriteObject):
self.widgets["sprite"]["spriteObject"] = spriteObject
def sprite_select():
spriteSelector.SpriteSelector(parent, partial(set_sprite, spriteSetter=sprite_setter,
spriteNameVar=self.widgets["sprite"]["spriteNameVar"],
randomSpriteVar=top.randomSprite))
spriteSelectButton = Button(spriteDialogFrame, text='...', command=sprite_select)
baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
spriteDialogFrame.pack(anchor=E)
return self
def set_sprite(sprite_param, random_sprite=False, spriteSetter=None, spriteNameVar=None, randomSpriteVar=None):
if sprite_param is None or not sprite_param.valid:
if spriteSetter:
spriteSetter(None)
if spriteNameVar is not None:
spriteNameVar.set('(unchanged)')
else:
if spriteSetter:
spriteSetter(sprite_param)
if spriteNameVar is not None:
spriteNameVar.set(sprite_param.name)
if randomSpriteVar:
randomSpriteVar.set(random_sprite)

View File

@@ -0,0 +1,45 @@
import os
from tkinter import ttk, filedialog, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, E, W, LEFT, RIGHT, X
import gui.widgets as widgets
import json
import os
def generation_page(parent,settings):
# Generation Setup
self = ttk.Frame(parent)
# Generation Setup options
self.widgets = {}
# Generation Setup option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","generation","checkboxes.json")) as checkboxes:
myDict = json.load(checkboxes)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["checkboxes"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(anchor=W)
self.frames["baserom"] = Frame(self)
self.frames["baserom"].pack(anchor=W, fill=X)
## Locate base ROM
baseRomFrame = Frame(self.frames["baserom"])
baseRomLabel = Label(baseRomFrame, text='Base Rom: ')
self.romVar = StringVar()
romEntry = Entry(baseRomFrame, textvariable=self.romVar)
self.romVar.set(settings["rom"])
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")], initialdir=os.path.join("."))
self.romVar.set(rom)
romSelectButton = Button(baseRomFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, fill=X, expand=True)
romSelectButton.pack(side=LEFT)
baseRomFrame.pack(fill=X)
return self,settings

35
gui/randomize/item.py Normal file
View File

@@ -0,0 +1,35 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def item_page(parent):
# Item Randomizer
self = ttk.Frame(parent)
# Item Randomizer options
self.widgets = {}
# Item Randomizer option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["leftItemFrame"] = Frame(self)
self.frames["rightItemFrame"] = Frame(self)
self.frames["leftItemFrame"].pack(side=LEFT)
self.frames["rightItemFrame"].pack(side=RIGHT)
with open(os.path.join("resources","app","gui","randomize","item","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
return self

View File

@@ -0,0 +1,38 @@
from tkinter import ttk, StringVar, Entry, Frame, Label, Spinbox, N, E, W, X, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def multiworld_page(parent,settings):
# Multiworld
self = ttk.Frame(parent)
# Multiworld options
self.widgets = {}
# Multiworld option sections
self.frames = {}
self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W, fill=X)
with open(os.path.join("resources","app","gui","randomize","multiworld","widgets.json")) as multiworldItems:
myDict = json.load(multiworldItems)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(side=LEFT, anchor=N)
## List of Player Names
key = "names"
self.widgets[key] = Frame(self.frames["widgets"])
self.widgets[key].label = Label(self.widgets[key], text='Player names')
self.widgets[key].storageVar = StringVar(value=settings["names"])
def saveMultiNames(caller,_,mode):
settings["names"] = self.widgets[key].storageVar.get()
self.widgets[key].storageVar.trace_add("write",saveMultiNames)
self.widgets[key].textbox = Entry(self.widgets[key], textvariable=self.widgets[key].storageVar)
self.widgets[key].label.pack(side=LEFT)
self.widgets[key].textbox.pack(side=LEFT, fill=X, expand=True)
self.widgets[key].pack(anchor=N, fill=X, expand=True)
return self,settings

View File

@@ -0,0 +1 @@
# do nothing, just exist to make "gui.startinventory" package

View File

@@ -0,0 +1,63 @@
from tkinter import ttk, StringVar, Entry, Frame, Label, N, E, W, LEFT, RIGHT, X, VERTICAL, Y
import gui.widgets as widgets
import json
import os
import classes.constants as CONST
def startinventory_page(top,parent):
# Starting Inventory
self = ttk.Frame(parent)
def create_list_frame(parent, framename):
parent.frames[framename] = Frame(parent)
parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N)
parent.frames[framename].thisRow = 0
parent.frames[framename].thisCol = 0
def create_vertical_rule(num=1):
for i in range(0,num):
ttk.Separator(self, orient=VERTICAL).pack(side=LEFT, anchor=N, fill=Y)
def validation(P):
if str.isdigit(P) or P == "":
return True
else:
return False
vcmd=(self.register(validation), '%P')
# Starting Inventory options
self.startingWidgets = {}
# Starting Inventory option sections
self.frames = {}
create_list_frame(self,"itemList1")
create_vertical_rule(2)
create_list_frame(self,"itemList2")
create_vertical_rule(2)
create_list_frame(self,"itemList3")
create_vertical_rule(2)
create_list_frame(self,"itemList4")
create_vertical_rule(2)
create_list_frame(self,"itemList5")
with open(os.path.join("resources","app","gui","custom","overview","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for key in CONST.CANTSTARTWITH:
for num in range(1, 5 + 1):
thisList = "itemList" + str(num)
if key in myDict[thisList]:
del myDict[thisList][key]
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.startingWidgets[key] = dictWidgets[key]
for key in CONST.CUSTOMITEMS:
if key not in CONST.CANTSTARTWITH:
val = 0
if key in top.settings["startinventoryarray"]:
val = top.settings["startinventoryarray"][key]
self.startingWidgets[key].storageVar.set(val)
return self

145
gui/widgets.py Normal file
View File

@@ -0,0 +1,145 @@
from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, RIGHT, X
class Empty():
pass
class mySpinbox(Spinbox):
def __init__(self, *args, **kwargs):
Spinbox.__init__(self, *args, **kwargs)
self.bind('<MouseWheel>', self.mouseWheel)
self.bind('<Button-4>', self.mouseWheel)
self.bind('<Button-5>', self.mouseWheel)
def mouseWheel(self, event):
if event.num == 5 or event.delta == -120:
self.invoke('buttondown')
elif event.num == 4 or event.delta == 120:
self.invoke('buttonup')
def make_checkbox(self, parent, label, storageVar, manager, managerAttrs):
self = Frame(parent, name="checkframe-" + label.lower())
self.storageVar = storageVar
self.checkbox = Checkbutton(self, text=label, variable=self.storageVar, name="checkbox-" + label.lower())
if managerAttrs is not None:
self.checkbox.pack(managerAttrs)
else:
self.checkbox.pack()
return self
def make_selectbox(self, parent, label, options, storageVar, manager, managerAttrs):
def change_storage(*args):
self.storageVar.set(options[self.labelVar.get()])
def change_selected(*args):
keys = options.keys()
vals = options.values()
keysList = list(keys)
valsList = list(vals)
self.labelVar.set(keysList[valsList.index(str(self.storageVar.get()))])
self = Frame(parent, name="selectframe-" + label.lower())
self.storageVar = storageVar
self.storageVar.trace_add("write",change_selected)
self.labelVar = StringVar()
self.labelVar.trace_add("write",change_storage)
self.label = Label(self, text=label)
if managerAttrs is not None and "label" in managerAttrs:
self.label.pack(managerAttrs["label"])
else:
self.label.pack()
self.selectbox = OptionMenu(self, self.labelVar, *options.keys())
self.selectbox.config(width=20)
self.labelVar.set(managerAttrs["default"] if "default" in managerAttrs else list(options.keys())[0])
if managerAttrs is not None and "selectbox" in managerAttrs:
self.selectbox.pack(managerAttrs["selectbox"])
else:
self.selectbox.pack()
return self
def make_spinbox(self, parent, label, storageVar, manager, managerAttrs):
self = Frame(parent, name="spinframe-" + label.lower())
self.storageVar = storageVar
self.label = Label(self, text=label)
if managerAttrs is not None and "label" in managerAttrs:
self.label.pack(managerAttrs["label"])
else:
self.label.pack()
fromNum = 1
toNum = 100
if "spinbox" in managerAttrs:
if "from" in managerAttrs:
fromNum = managerAttrs["spinbox"]["from"]
if "to" in managerAttrs:
toNum = managerAttrs["spinbox"]["to"]
self.spinbox = mySpinbox(self, from_=fromNum, to=toNum, width=5, textvariable=self.storageVar, name="spinbox-" + label.lower())
if managerAttrs is not None and "spinbox" in managerAttrs:
self.spinbox.pack(managerAttrs["spinbox"])
else:
self.spinbox.pack()
return self
def make_textbox(self, parent, label, storageVar, manager, managerAttrs):
widget = Empty()
widget.storageVar = storageVar
widget.label = Label(parent, text=label)
widget.textbox = Entry(parent, justify=RIGHT, textvariable=widget.storageVar, width=3)
if "default" in managerAttrs:
widget.storageVar.set(managerAttrs["default"])
# grid
if manager == "grid":
widget.label.grid(managerAttrs["label"] if managerAttrs is not None and "label" in managerAttrs else None, row=parent.thisRow, column=parent.thisCol)
parent.thisCol += 1
widget.textbox.grid(managerAttrs["textbox"] if managerAttrs is not None and "textbox" in managerAttrs else None, row=parent.thisRow, column=parent.thisCol)
parent.thisRow += 1
parent.thisCol = 0
# pack
elif manager == "pack":
widget.label.pack(managerAttrs["label"] if managerAttrs is not None and "label" in managerAttrs else None)
widget.textbox.pack(managerAttrs["textbox"] if managerAttrs is not None and "textbox" in managerAttrs else None)
return widget
def make_widget(self, type, parent, label, storageVar=None, manager=None, managerAttrs=dict(), options=None):
widget = None
if manager is None:
manager = "pack"
thisStorageVar = storageVar
if isinstance(storageVar,str):
if storageVar == "int" or storageVar == "integer":
thisStorageVar = IntVar()
elif storageVar == "str" or storageVar == "string":
thisStorageVar = StringVar()
if type == "checkbox":
if thisStorageVar is None:
thisStorageVar = IntVar()
widget = make_checkbox(self, parent, label, thisStorageVar, manager, managerAttrs)
elif type == "selectbox":
if thisStorageVar is None:
thisStorageVar = StringVar()
widget = make_selectbox(self, parent, label, options, thisStorageVar, manager, managerAttrs)
elif type == "spinbox":
if thisStorageVar is None:
thisStorageVar = StringVar()
widget = make_spinbox(self, parent, label, thisStorageVar, manager, managerAttrs)
elif type == "textbox":
if thisStorageVar is None:
thisStorageVar = StringVar()
widget = make_textbox(self, parent, label, thisStorageVar, manager, managerAttrs)
widget.type = type
return widget
def make_widget_from_dict(self, defn, parent):
type = defn["type"] if "type" in defn else None
label = defn["label"]["text"] if "label" in defn and "text" in defn["label"] else ""
manager = defn["manager"] if "manager" in defn else None
managerAttrs = defn["managerAttrs"] if "managerAttrs" in defn else None
options = defn["options"] if "options" in defn else None
widget = make_widget(self, type, parent, label, None, manager, managerAttrs, options)
return widget
def make_widgets_from_dict(self, defns, parent):
widgets = {}
for key,defn in defns.items():
widgets[key] = make_widget_from_dict(self, defn, parent)
return widgets

View File

@@ -0,0 +1,124 @@
{
"checkboxes": {
"nobgm": {
"type": "checkbox",
"label": {
"text": "Disable Music & MSU-1"
}
},
"quickswap": {
"type": "checkbox",
"label": {
"text": "L/R Quickswapping"
}
}
},
"leftAdjustFrame": {
"heartcolor": {
"type": "selectbox",
"label": {
"text": "Heart Color"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Red": "red",
"Blue": "blue",
"Green": "green",
"Yellow": "yellow",
"Random": "random"
}
},
"heartbeep": {
"type": "selectbox",
"label": {
"text": "Heart Beep sound rate"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Double": "double",
"Normal": "normal",
"Half": "half",
"Quarter": "quarter",
"Off": "off"
}
}
},
"rightAdjustFrame": {
"menuspeed": {
"type": "selectbox",
"label": {
"text": "Menu Speed"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Instant": "instant",
"Quadruple": "quadruple",
"Triple": "triple",
"Double": "double",
"Normal": "normal",
"Half": "half"
}
},
"owpalettes": {
"type": "selectbox",
"label": {
"text": "Overworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
},
"uwpalettes": {
"type": "selectbox",
"label": {
"text": "Underworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
}
}
}

View File

@@ -0,0 +1,935 @@
{
"itemList1": {
"bow": {
"type": "textbox",
"label": {
"text": "Bow"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressivebow": {
"type": "textbox",
"label": {
"text": "Progressive Bow"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"boomerang": {
"type": "textbox",
"label": {
"text": "Blue Boomerang"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"redmerang": {
"type": "textbox",
"label": {
"text": "Red Boomerang"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"hookshot": {
"type": "textbox",
"label": {
"text": "Hookshot"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"mushroom": {
"type": "textbox",
"label": {
"text": "Mushroom"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"powder": {
"type": "textbox",
"label": {
"text": "Magic Powder"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"firerod": {
"type": "textbox",
"label": {
"text": "Fire Rod"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"icerod": {
"type": "textbox",
"label": {
"text": "Ice Rod"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"bombos": {
"type": "textbox",
"label": {
"text": "Bombos"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"ether": {
"type": "textbox",
"label": {
"text": "Ether"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"quake": {
"type": "textbox",
"label": {
"text": "Quake"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"lamp": {
"type": "textbox",
"label": {
"text": "Lamp"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"hammer": {
"type": "textbox",
"label": {
"text": "Hammer"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"shovel": {
"type": "textbox",
"label": {
"text": "Shovel"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
}
},
"itemList2": {
"flute": {
"type": "textbox",
"label": {
"text": "Flute"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"bugnet": {
"type": "textbox",
"label": {
"text": "Bug Net"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"book": {
"type": "textbox",
"label": {
"text": "Book"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"bottle": {
"type": "textbox",
"label": {
"text": "Bottle"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 4
}
},
"somaria": {
"type": "textbox",
"label": {
"text": "Cane of Somaria"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"byrna": {
"type": "textbox",
"label": {
"text": "Cane of Byrna"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"cape": {
"type": "textbox",
"label": {
"text": "Magic Cape"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"mirror": {
"type": "textbox",
"label": {
"text": "Magic Mirror"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"boots": {
"type": "textbox",
"label": {
"text": "Pegasus Boots"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"powerglove": {
"type": "textbox",
"label": {
"text": "Power Glove"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"titansmitt": {
"type": "textbox",
"label": {
"text": "Titan's Mitt"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressiveglove": {
"type": "textbox",
"label": {
"text": "Progressive Glove"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"flippers": {
"type": "textbox",
"label": {
"text": "Flippers"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"pearl": {
"type": "textbox",
"label": {
"text": "Moon Pearl"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"heartpiece": {
"type": "textbox",
"label": {
"text": "Piece of Heart"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 24
}
}
},
"itemList3": {
"heartcontainer": {
"type": "textbox",
"label": {
"text": "Heart Container"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 10
}
},
"sancheart": {
"type": "textbox",
"label": {
"text": "Sanctuary Heart"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"sword1": {
"type": "textbox",
"label": {
"text": "Fighters' Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"sword2": {
"type": "textbox",
"label": {
"text": "Master Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"sword3": {
"type": "textbox",
"label": {
"text": "Tempered Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"sword4": {
"type": "textbox",
"label": {
"text": "Golden Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressivesword": {
"type": "textbox",
"label": {
"text": "Progressive Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 4
}
},
"shield1": {
"type": "textbox",
"label": {
"text": "Fighters' Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"shield2": {
"type": "textbox",
"label": {
"text": "Fire Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"shield3": {
"type": "textbox",
"label": {
"text": "Mirror Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressiveshield": {
"type": "textbox",
"label": {
"text": "Progressive Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 3
}
},
"mail2": {
"type": "textbox",
"label": {
"text": "Blue Mail"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"mail3": {
"type": "textbox",
"label": {
"text": "Red Mail"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressivemail": {
"type": "textbox",
"label": {
"text": "Progressive Mail"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"halfmagic": {
"type": "textbox",
"label": {
"text": "Half Magic"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
}
},
"itemList4": {
"quartermagic": {
"type": "textbox",
"label": {
"text": "Quarter Magic"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"bombsplus5": {
"type": "textbox",
"label": {
"text": "Bomb Cap +5"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"bombsplus10": {
"type": "textbox",
"label": {
"text": "Bomb Cap +10"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"arrowsplus5": {
"type": "textbox",
"label": {
"text": "Arrow Cap +5"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"arrowsplus10": {
"type": "textbox",
"label": {
"text": "Arrow Cap +10"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"arrow1": {
"type": "textbox",
"label": {
"text": "Arrow (1)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"arrow10": {
"type": "textbox",
"label": {
"text": "Arrow (10)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 12
}
},
"bomb1": {
"type": "textbox",
"label": {
"text": "Bomb (1)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"bomb3": {
"type": "textbox",
"label": {
"text": "Bomb (3)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 16
}
},
"bomb10": {
"type": "textbox",
"label": {
"text": "Bomb (10)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"rupee1": {
"type": "textbox",
"label": {
"text": "Rupee (1)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"rupee5": {
"type": "textbox",
"label": {
"text": "Rupee (5)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 4
}
},
"rupee20": {
"type": "textbox",
"label": {
"text": "Rupee (20)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 28
}
},
"rupee50": {
"type": "textbox",
"label": {
"text": "Rupee (50)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 7
}
},
"rupee100": {
"type": "textbox",
"label": {
"text": "Rupee (100)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
}
},
"itemList5": {
"rupee300": {
"type": "textbox",
"label": {
"text": "Rupee (300)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 5
}
},
"blueclock": {
"type": "textbox",
"label": {
"text": "Blue Clock"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"greenclock": {
"type": "textbox",
"label": {
"text": "Green Clock"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"redclock": {
"type": "textbox",
"label": {
"text": "Red Clock"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"silversupgrade": {
"type": "textbox",
"label": {
"text": "Silver Arrows Upgrade"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"generickeys": {
"type": "textbox",
"label": {
"text": "Generic Keys"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"triforcepieces": {
"type": "textbox",
"label": {
"text": "Triforce Pieces"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"triforcepiecesgoal": {
"type": "textbox",
"label": {
"text": "Triforce Pieces Goal"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"triforce": {
"type": "textbox",
"label": {
"text": "Triforce (win game)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"rupoor": {
"type": "textbox",
"label": {
"text": "Rupoor"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"rupoorcost": {
"type": "textbox",
"label": {
"text": "Rupoor Cost"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 10
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"mapshuffle": {
"type": "checkbox",
"label": {
"text": "Maps"
}
},
"compassshuffle": {
"type": "checkbox",
"label": {
"text": "Compasses"
}
},
"smallkeyshuffle": {
"type": "checkbox",
"label": {
"text": "Small Keys"
}
},
"bigkeyshuffle": {
"type": "checkbox",
"label": {
"text": "Big Keys"
}
}
}

View File

@@ -0,0 +1,49 @@
{
"dungeondoorshuffle": {
"type": "selectbox",
"label": {
"text": "Dungeon Door Shuffle"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Basic"
},
"options": {
"Vanilla": "vanilla",
"Basic": "basic",
"Crossed": "crossed"
}
},
"experimental": {
"type": "checkbox",
"label": {
"text": "Enable Experimental Features"
}
},
"dungeon_counters": {
"type": "selectbox",
"label": {
"text": "Dungeon Chest Counters"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Auto"
},
"options": {
"Auto": "default",
"Off": "off",
"On": "on",
"On Compass Pickup": "pickup"
}
}
}

View File

@@ -0,0 +1,93 @@
{
"checkboxes": {
"potshuffle": {
"type": "checkbox",
"label": {
"text": "Pot Shuffle"
}
}
},
"leftEnemizerFrame": {
"enemyshuffle": {
"type": "selectbox",
"label": {
"text": "Enemy Shuffle"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "none",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
},
"bossshuffle": {
"type": "selectbox",
"label": {
"text": "Boss Shuffle"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "none",
"Basic": "basic",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
}
},
"rightEnemizerFrame": {
"enemydamage": {
"type": "selectbox",
"label": {
"text": "Enemy Damage"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "default",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
},
"enemyhealth": {
"type": "selectbox",
"label": {
"text": "Enemy Health"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "default",
"Easy": "easy",
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
}
}
}

View File

@@ -0,0 +1,40 @@
{
"widgets": {
"openpyramid": {
"type": "checkbox",
"label": {
"text": "Pre-open Pyramid Hole"
}
},
"shuffleganon": {
"type": "checkbox",
"label": {
"text": "Include Ganon's Tower and Pyramid Hole in shuffle pool"
}
},
"entranceshuffle": {
"type": "selectbox",
"label": {
"text": "Entrance Shuffle"
},
"managerAttrs": {
"label": { "side": "left" },
"selectbox": { "side": "right" }
},
"options": {
"Vanilla": "vanilla",
"Simple": "simple",
"Restricted": "restricted",
"Full": "full",
"Crossed": "crossed",
"Insanity": "insanity",
"Restricted (Legacy)": "restricted_legacy",
"Full (Legacy)": "full_legacy",
"Madness (Legacy)": "madness_legacy",
"Insanity (Legacy)": "insanity_legacy",
"Dungeons + Full": "dungeonsfull",
"Dungeons + Simple": "dungeonssimple"
}
}
}
}

View File

@@ -0,0 +1,131 @@
{
"checkboxes": {
"hints": {
"type": "checkbox",
"label": {
"text": "Include Helpful Hints"
},
"default": "true"
},
"nobgm": {
"type": "checkbox",
"label": {
"text": "Disable Music & MSU-1"
}
},
"quickswap": {
"type": "checkbox",
"label": {
"text": "L/R Quickswapping"
}
}
},
"leftRomOptionsFrame": {
"heartcolor": {
"type": "selectbox",
"label": {
"text": "Heart Color"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Red": "red",
"Blue": "blue",
"Green": "green",
"Yellow": "yellow",
"Random": "random"
}
},
"heartbeep": {
"type": "selectbox",
"label": {
"text": "Heart Beep sound rate"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Double": "double",
"Normal": "normal",
"Half": "half",
"Quarter": "quarter",
"Off": "off"
}
}
},
"rightRomOptionsFrame": {
"menuspeed": {
"type": "selectbox",
"label": {
"text": "Menu Speed"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Instant": "instant",
"Quadruple": "quadruple",
"Triple": "triple",
"Double": "double",
"Normal": "normal",
"Half": "half"
}
},
"owpalettes": {
"type": "selectbox",
"label": {
"text": "Overworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
},
"uwpalettes": {
"type": "selectbox",
"label": {
"text": "Underworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"spoiler": {
"type": "checkbox",
"label": {
"text": "Create Spoiler Log"
}
},
"suppressrom": {
"type": "checkbox",
"label": {
"text": "Do not create patched ROM"
}
},
"usestartinventory": {
"type": "checkbox",
"label": {
"text": "Use starting inventory"
}
},
"usecustompool": {
"type": "checkbox",
"label": {
"text": "Use custom item pool"
}
},
"saveonexit": {
"type": "selectbox",
"label": {
"text": "Save Settings on Exit"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Ask Me": "ask",
"Always": "always",
"Never": "never"
}
}
}

View File

@@ -0,0 +1,266 @@
{
"checkboxes": {
"retro": {
"type": "checkbox",
"label": {
"text": "Retro mode (universal keys)"
}
}
},
"leftItemFrame": {
"worldstate": {
"type": "selectbox",
"label": {
"text": "World State"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Open"
},
"options": {
"Standard": "standard",
"Open": "open",
"Inverted": "inverted"
}
},
"logiclevel": {
"type": "selectbox",
"label": {
"text": "Logic Level"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"No Glitches": "noglitches",
"Minor Glitches": "minorglitches",
"No Logic": "nologic"
}
},
"goal": {
"type": "selectbox",
"label": {
"text": "Goal"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Defeat Ganon": "ganon",
"Master Sword Pedestal": "pedestal",
"All Dungeons": "dungeons",
"Triforce Hunt": "triforcehunt",
"Crystals": "crystals"
}
},
"crystals_gt": {
"type": "selectbox",
"label": {
"text": "Crystals to open GT"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"Random": "random"
}
},
"crystals_ganon": {
"type": "selectbox",
"label": {
"text": "Crystals to harm Ganon"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"Random": "random"
}
},
"weapons": {
"type": "selectbox",
"label": {
"text": "Weapons"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Randomized": "random",
"Assured": "assured",
"Swordless": "swordless",
"Vanilla": "vanilla"
}
}
},
"rightItemFrame": {
"itempool": {
"type": "selectbox",
"label": {
"text": "Item Pool"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
},
"itemfunction": {
"type": "selectbox",
"label": {
"text": "Item Functionality"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
},
"timer": {
"type": "selectbox",
"label": {
"text": "Timer Setting"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"No Timer": "none",
"Stopwatch": "display",
"Timed": "timed",
"Timed OHKO": "timed-ohko",
"OHKO": "ohko",
"Timed Countdown": "timed-countdown"
}
},
"progressives": {
"type": "selectbox",
"label": {
"text": "Progressive Items"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"On": "on",
"Off": "off",
"Random": "random"
}
},
"accessibility": {
"type": "selectbox",
"label": {
"text": "Accessibility"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"100% Inventory": "items",
"100% Locations": "locations",
"Beatable": "none"
}
},
"sortingalgo": {
"type": "selectbox",
"label": {
"text": "Item Sorting"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Balanced"
},
"options": {
"Freshness": "freshness",
"Flood": "flood",
"VT8.21": "vt21",
"VT8.22": "vt22",
"VT8.25": "vt25",
"VT8.26": "vt26",
"Balanced": "balanced"
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"worlds": {
"type": "spinbox",
"label": {
"text": "Worlds"
},
"managerAttrs": {
"label": {
"side": "left"
},
"spinbox": {
"side": "right"
}
}
}
}