Merge branch 'translation' into ci-dev

This commit is contained in:
Mike A. Trethewey
2020-03-10 21:09:20 -07:00
28 changed files with 1831 additions and 672 deletions

View File

@@ -4,6 +4,7 @@ import logging
import json import json
from collections import OrderedDict, deque, defaultdict from collections import OrderedDict, deque, defaultdict
from source.classes.BabelFish import BabelFish
from EntranceShuffle import door_addresses from EntranceShuffle import door_addresses
from _vendor.collections_extended import bag from _vendor.collections_extended import bag
from Utils import int16_as_bytes from Utils import int16_as_bytes
@@ -71,6 +72,7 @@ class World(object):
self.key_logic = {} self.key_logic = {}
self.pool_adjustment = {} self.pool_adjustment = {}
self.key_layout = defaultdict(dict) self.key_layout = defaultdict(dict)
self.fish = BabelFish()
for player in range(1, players + 1): for player in range(1, players + 1):
# If World State is Retro, set to Open and set Retro flag # If World State is Retro, set to Open and set Retro flag
@@ -1732,43 +1734,60 @@ class Spoiler(object):
outfile.write('\n\nDoors:\n\n') outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join( outfile.write('\n'.join(
['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', ['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '',
entry['entrance'], self.world.fish.translate("meta","doors",entry['entrance']),
'<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>',
entry['exit'], self.world.fish.translate("meta","doors",entry['exit']),
'({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for '({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for
entry in self.doors.values()])) entry in self.doors.values()]))
if self.doorTypes: if self.doorTypes:
# doorNames: For some reason these come in combined, somehow need to split on the thing to translate
# doorTypes: Small Key, Bombable, Bonkable
outfile.write('\n\nDoor Types:\n\n') outfile.write('\n\nDoor Types:\n\n')
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()])) outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta","doors",entry['doorNames']), self.world.fish.translate("meta","doorTypes",entry['type'])) for entry in self.doorTypes.values()]))
if self.entrances: if self.entrances:
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
outfile.write('\n\nEntrances:\n\n') outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()])) outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()]))
outfile.write('\n\nMedallions:\n') outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items(): for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}') outfile.write(f'\n{dungeon}: {medallion} Medallion')
if self.startinventory: if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n') outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory)) outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write('\n\nPaths:\n\n')
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta","locations",location), self.world.fish.translate("meta","items",item)) for grouping in self.locations.values() for (location, item) in grouping.items()]))
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta","locations",shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta","items",item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (self.world.fish.translate("meta","locations",location), self.world.fish.translate("meta","items",item)) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta","items",unreachable.item), self.world.fish.translate("meta","locations",unreachable)) for unreachable in self.unreachables]))
# rooms: Change up room names; only if it's got no locations in it
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
outfile.write('\n\nPaths:\n\n')
path_listings = [] path_listings = []
for location, path in sorted(self.paths.items()): for location, path in sorted(self.paths.items()):
path_lines = [] path_lines = []
for region, exit in path: for region, exit in path:
if exit is not None: if exit is not None:
path_lines.append("{} -> {}".format(region, exit)) path_lines.append("{} -> {}".format(self.world.fish.translate("meta","rooms",region), self.world.fish.translate("meta","entrances",exit)))
else: else:
path_lines.append(region) path_lines.append(self.world.fish.translate("meta","rooms",region))
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) path_listings.append("{}\n {}".format(self.world.fish.translate("meta","locations",location), "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))

446
CLI.py
View File

@@ -8,9 +8,8 @@ import textwrap
import shlex import shlex
import sys import sys
from Main import main
import source.classes.constants as CONST import source.classes.constants as CONST
from source.classes.BabelFish import BabelFish
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -25,271 +24,56 @@ def parse_arguments(argv, no_defaults=False):
# get settings # get settings
settings = get_settings() settings = get_settings()
lang = "en"
if argv is not None:
priority = get_args_priority(None, None, argv)
if "load" in priority:
priority = priority["load"]
if "lang" in priority:
lang = priority["lang"]
fish = BabelFish(lang=lang)
# we need to know how many players we have first # we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args(argv) multiargs, _ = parser.parse_known_args(argv)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) 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'], # get args
help='''\ args = []
Select Enforcement of Item Requirements. (default: %(default)s) with open(os.path.join("resources","app","cli","args.json")) as argsFile:
No Glitches: args = json.load(argsFile)
Minor Glitches: May require Fake Flippers, Bunny Revival for arg in args:
and Dark Room Navigation. argdata = args[arg]
No Logic: Distribute items without regard for argname = "--" + arg
item requirements. argatts = {}
''') argatts["help"] = "(default: %(default)s)"
parser.add_argument('--mode', default=defval(settings["mode"]), const='open', nargs='?', choices=['standard', 'open', 'inverted'], if "action" in argdata:
help='''\ argatts["action"] = argdata["action"]
Select game mode. (default: %(default)s) if "choices" in argdata:
Open: World starts with Zelda rescued. argatts["choices"] = argdata["choices"]
Standard: Fixes Hyrule Castle Secret Entrance and Front Door argatts["const"] = argdata["choices"][0]
but may lead to weird rain state issues if you exit argatts["default"] = argdata["choices"][0]
through the Hyrule Castle side exits before rescuing argatts["nargs"] = "?"
Zelda in a full shuffle. elif arg in settings:
Inverted: Starting locations are Dark Sanctuary in West Dark argatts["default"] = defval(settings[arg] != 0) if "type" in argdata and argdata["type"] == "bool" else defval(settings[arg])
World or at Link's House, which is shuffled freely. arghelp = fish.translate("cli","help",arg)
Requires the moon pearl to be Link in the Light World if "help" in argdata and argdata["help"] == "suppress":
instead of a bunny. argatts["help"] = argparse.SUPPRESS
''') elif not isinstance(arghelp,str):
parser.add_argument('--swords', default=defval(settings["swords"]), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'], argatts["help"] = '\n'.join(arghelp).replace("\\'","'")
help='''\ parser.add_argument(argname,**argatts)
Select sword placement. (default: %(default)s)
Random: All swords placed randomly. parser.add_argument('--seed', default=defval(int(settings["seed"]) if settings["seed"] != "" and settings["seed"] is not None else None), help="\n".join(fish.translate("cli","help","seed")), type=int)
Assured: Start game with a sword already. parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli","help","count")), type=int)
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS)
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 # 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('--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('--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('--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: if multiargs.multi:
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
@@ -324,6 +108,7 @@ def parse_arguments(argv, no_defaults=False):
def get_settings(): def get_settings():
# set default settings # set default settings
settings = { settings = {
"lang": "en",
"retro": False, "retro": False,
"mode": "open", "mode": "open",
"logic": "noglitches", "logic": "noglitches",
@@ -378,88 +163,89 @@ def get_settings():
"custom": False, "custom": False,
"rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"), "rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"),
"seed": None, "seed": "",
"count": None, "count": 1,
"startinventory": "", "startinventory": "",
"beemizer": 0, "beemizer": 0,
"remote_items": False, "remote_items": False,
"race": False, "race": False,
"customitemarray": { "customitemarray": {
"bow": 0, "bow": 0,
"progressivebow": 2, "progressivebow": 2,
"boomerang": 1, "boomerang": 1,
"redmerang": 1, "redmerang": 1,
"hookshot": 1, "hookshot": 1,
"mushroom": 1, "mushroom": 1,
"powder": 1, "powder": 1,
"firerod": 1, "firerod": 1,
"icerod": 1, "icerod": 1,
"bombos": 1, "bombos": 1,
"ether": 1, "ether": 1,
"quake": 1, "quake": 1,
"lamp": 1, "lamp": 1,
"hammer": 1, "hammer": 1,
"shovel": 1, "shovel": 1,
"flute": 1, "flute": 1,
"bugnet": 1, "bugnet": 1,
"book": 1, "book": 1,
"bottle": 4, "bottle": 4,
"somaria": 1, "somaria": 1,
"byrna": 1, "byrna": 1,
"cape": 1, "cape": 1,
"mirror": 1, "mirror": 1,
"boots": 1, "boots": 1,
"powerglove": 0, "powerglove": 0,
"titansmitt": 0, "titansmitt": 0,
"progressiveglove": 2, "progressiveglove": 2,
"flippers": 1, "flippers": 1,
"pearl": 1, "pearl": 1,
"heartpiece": 24, "heartpiece": 24,
"heartcontainer": 10, "heartcontainer": 10,
"sancheart": 1, "sancheart": 1,
"sword1": 0, "sword1": 0,
"sword2": 0, "sword2": 0,
"sword3": 0, "sword3": 0,
"sword4": 0, "sword4": 0,
"progressivesword": 4, "progressivesword": 4,
"shield1": 0, "shield1": 0,
"shield2": 0, "shield2": 0,
"shield3": 0, "shield3": 0,
"progressiveshield": 3, "progressiveshield": 3,
"mail2": 0, "mail2": 0,
"mail3": 0, "mail3": 0,
"progressivemail": 2, "progressivemail": 2,
"halfmagic": 1, "halfmagic": 1,
"quartermagic": 0, "quartermagic": 0,
"bombsplus5": 0, "bombsplus5": 0,
"bombsplus10": 0, "bombsplus10": 0,
"arrowsplus5": 0, "arrowsplus5": 0,
"arrowsplus10": 0, "arrowsplus10": 0,
"arrow1": 1, "arrow1": 1,
"arrow10": 12, "arrow10": 12,
"bomb1": 0, "bomb1": 0,
"bomb3": 16, "bomb3": 16,
"bomb10": 1, "bomb10": 1,
"rupee1": 2, "rupee1": 2,
"rupee5": 4, "rupee5": 4,
"rupee20": 28, "rupee20": 28,
"rupee50": 7, "rupee50": 7,
"rupee100": 1, "rupee100": 1,
"rupee300": 5, "rupee300": 5,
"blueclock": 0, "blueclock": 0,
"greenclock": 0, "greenclock": 0,
"redclock": 0, "redclock": 0,
"silversupgrade": 0, "silversupgrade": 0,
"generickeys": 0, "generickeys": 0,
"triforcepieces": 0, "triforcepieces": 0,
"triforcepiecesgoal": 0, "triforcepiecesgoal": 0,
"triforce": 0, "triforce": 0,
"rupoor": 0, "rupoor": 0,
"rupoorcost": 10 "rupoorcost": 10
}, },
"randomSprite": False, "randomSprite": False,
"outputpath": os.path.join("."), "outputpath": os.path.join("."),
"saveonexit": "ask", "saveonexit": "ask",
"outputname": "",
"startinventoryarray": {} "startinventoryarray": {}
} }

View File

@@ -316,7 +316,7 @@ def within_dungeon(world, player):
dungeon_builders[key] = simple_dungeon_builder(key, sector_list) dungeon_builders[key] = simple_dungeon_builder(key, sector_list)
dungeon_builders[key].entrance_list = list(entrances_map[key]) dungeon_builders[key].entrance_list = list(entrances_map[key])
recombinant_builders = {} recombinant_builders = {}
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, world.fish)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
paths = determine_required_paths(world, player) paths = determine_required_paths(world, player)
@@ -326,15 +326,15 @@ def within_dungeon(world, player):
start = time.process_time() start = time.process_time()
for builder in world.dungeon_layouts[player].values(): for builder in world.dungeon_layouts[player].values():
shuffle_key_doors(builder, world, player) shuffle_key_doors(builder, world, player)
logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start) logging.getLogger('').info('%s: %s', world.fish.translate("cli","cli","keydoor.shuffle.time"), time.process_time()-start)
smooth_door_pairs(world, player) smooth_door_pairs(world, player)
def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map): def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, fish):
for name, split_list in split_region_starts.items(): for name, split_list in split_region_starts.items():
builder = dungeon_builders.pop(name) builder = dungeon_builders.pop(name)
recombinant_builders[name] = builder recombinant_builders[name] = builder
split_builders = split_dungeon_builder(builder, split_list) split_builders = split_dungeon_builder(builder, split_list, fish)
dungeon_builders.update(split_builders) dungeon_builders.update(split_builders)
for sub_name, split_entrances in split_list.items(): for sub_name, split_entrances in split_list.items():
sub_builder = dungeon_builders[name+' '+sub_name] sub_builder = dungeon_builders[name+' '+sub_name]
@@ -366,7 +366,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
sector_queue.append(builder) sector_queue.append(builder)
last_key = builder.name last_key = builder.name
else: else:
logging.getLogger('').info('Generating dungeon: %s', builder.name) logging.getLogger('').info('%s: %s', world.fish.translate("cli","cli","generating.dungeon"), builder.name)
ds = generate_dungeon(builder, origin_list_sans_drops, split_dungeon, world, player) 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, connections, potentials, enabled_entrances, world, player)
ds.name = name ds.name = name
@@ -671,7 +671,7 @@ def cross_dungeon(world, player):
key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name] key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name]
loc.forced_item = loc.item = ItemFactory(key_name, player) loc.forced_item = loc.item = ItemFactory(key_name, player)
recombinant_builders = {} recombinant_builders = {}
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, world.fish)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
@@ -792,7 +792,7 @@ def assign_cross_keys(dungeon_builders, world, player):
dungeon.small_keys = [] dungeon.small_keys = []
else: else:
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
logging.getLogger('').info('Cross Dungeon: Key door shuffle time: %s', time.process_time()-start) logging.getLogger('').info('%s: %s', world.fish.translate("cli","cli","keydoor.shuffle.time.crossed"), time.process_time()-start)
def reassign_boss(boss_region, boss_key, builder, gt, world, player): def reassign_boss(boss_region, boss_key, builder, gt, world, player):
@@ -949,14 +949,14 @@ def calc_used_dungeon_items(builder):
def find_valid_combination(builder, start_regions, world, player, drop_keys=True): def find_valid_combination(builder, start_regions, world, player, drop_keys=True):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Shuffling Key doors for %s', builder.name) logger.info('%s %s', world.fish.translate("cli","cli","shuffling.keydoors"), builder.name)
# find valid combination of candidates # find valid combination of candidates
if len(builder.candidates) < builder.key_doors_num: if len(builder.candidates) < builder.key_doors_num:
if not drop_keys: if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False return False
builder.key_doors_num = len(builder.candidates) # reduce number of key doors builder.key_doors_num = len(builder.candidates) # reduce number of key doors
logger.info('Lowering key door count because not enough candidates: %s', builder.name) logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.candidates"), builder.name)
combinations = ncr(len(builder.candidates), builder.key_doors_num) combinations = ncr(len(builder.candidates), builder.key_doors_num)
itr = 0 itr = 0
start = time.process_time() start = time.process_time()
@@ -976,7 +976,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
if not drop_keys: if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False return False
logger.info('Lowering key door count because no valid layouts: %s', builder.name) logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), builder.name)
builder.key_doors_num -= 1 builder.key_doors_num -= 1
if builder.key_doors_num < 0: if builder.key_doors_num < 0:
raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name) raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name)

View File

@@ -1149,8 +1149,8 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None):
# polarity: # polarity:
if not global_pole.is_valid(dungeon_map): if not global_pole.is_valid(dungeon_map):
raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!') raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!')
logger.info('-Balancing Doors') logger.info(world.fish.translate("cli","cli","balance.doors"))
assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, world.fish)
# the rest # the rest
assign_the_rest(dungeon_map, neutral_sectors, global_pole) assign_the_rest(dungeon_map, neutral_sectors, global_pole)
return dungeon_map return dungeon_map
@@ -1434,9 +1434,9 @@ def sum_polarity(sector_list):
return pol return pol
def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger): def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, fish):
# step 1: fix polarity connection issues # step 1: fix polarity connection issues
logger.info('--Basic Traversal') logger.info(fish.translate("cli","cli","basic.traversal"))
unconnected_builders = identify_polarity_issues(dungeon_map) unconnected_builders = identify_polarity_issues(dungeon_map)
while len(unconnected_builders) > 0: while len(unconnected_builders) > 0:
for name, builder in unconnected_builders.items(): for name, builder in unconnected_builders.items():
@@ -1474,7 +1474,7 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
problem_builders = identify_simple_branching_issues(problem_builders) problem_builders = identify_simple_branching_issues(problem_builders)
# step 3: fix neutrality issues # step 3: fix neutrality issues
polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger) polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger, fish)
# step 4: fix dead ends again # step 4: fix dead ends again
neutral_choices: List[List] = neutralize_the_rest(polarized_sectors) neutral_choices: List[List] = neutralize_the_rest(polarized_sectors)
@@ -1523,11 +1523,11 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
tries += 1 tries += 1
def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger): def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger, fish):
builder_order = list(dungeon_map.values()) builder_order = list(dungeon_map.values())
random.shuffle(builder_order) random.shuffle(builder_order)
for builder in builder_order: for builder in builder_order:
logger.info('--Balancing %s', builder.name) logger.info('%s %s', fish.translate("cli","cli","balancing"), builder.name)
while not builder.polarity().is_neutral(): while not builder.polarity().is_neutral():
candidates = find_neutralizing_candidates(builder, polarized_sectors) candidates = find_neutralizing_candidates(builder, polarized_sectors)
valid, sectors = False, None valid, sectors = False, None
@@ -1831,9 +1831,9 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole):
assign_sector(sector_list[i], builder, neutral_sectors, global_pole) assign_sector(sector_list[i], builder, neutral_sectors, global_pole)
def split_dungeon_builder(builder, split_list): def split_dungeon_builder(builder, split_list, fish):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Splitting Up Desert/Skull') logger.info(fish.translate("cli","cli","splitting.up") + ' ' + 'Desert/Skull')
candidate_sectors = dict.fromkeys(builder.sectors) candidate_sectors = dict.fromkeys(builder.sectors)
global_pole = GlobalPolarity(candidate_sectors) global_pole = GlobalPolarity(candidate_sectors)
@@ -1844,10 +1844,10 @@ def split_dungeon_builder(builder, split_list):
sub_builder.all_entrances = split_entrances sub_builder.all_entrances = split_entrances
for r_name in split_entrances: for r_name in split_entrances:
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole)
return balance_split(candidate_sectors, dungeon_map, global_pole) return balance_split(candidate_sectors, dungeon_map, global_pole, fish)
def balance_split(candidate_sectors, dungeon_map, global_pole): def balance_split(candidate_sectors, dungeon_map, global_pole, fish):
logger = logging.getLogger('') logger = logging.getLogger('')
# categorize sectors # categorize sectors
crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors) crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors)
@@ -1860,8 +1860,8 @@ def balance_split(candidate_sectors, dungeon_map, global_pole):
# blue barriers # blue barriers
assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole)
# polarity: # polarity:
logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') logger.info(fish.translate("cli","cli","re-balancing") + ' ' + next(iter(dungeon_map.keys())) + ' et al')
assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, fish)
# the rest # the rest
assign_the_rest(dungeon_map, neutral_sectors, global_pole) assign_the_rest(dungeon_map, neutral_sectors, global_pole)
return dungeon_map return dungeon_map

View File

@@ -8,7 +8,9 @@ import textwrap
import shlex import shlex
import sys import sys
from CLI import parse_arguments from source.classes.BabelFish import BabelFish
from CLI import parse_arguments, get_args_priority
from Main import main from Main import main
from Rom import get_sprite_from_name from Rom import get_sprite_from_name
from Utils import is_bundled, close_console from Utils import is_bundled, close_console
@@ -42,6 +44,12 @@ def start():
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel] loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel) logging.basicConfig(format='%(message)s', level=loglevel)
settings = get_args_priority(None, None, None)
lang = "en"
if "load" in settings and "lang" in settings["load"]:
lang = settings["load"]["lang"]
fish = BabelFish(lang=lang)
if args.gui: if args.gui:
from Gui import guiMain from Gui import guiMain
guiMain(args) guiMain(args)
@@ -51,11 +59,11 @@ def start():
logger = logging.getLogger('') logger = logging.getLogger('')
for _ in range(args.count): for _ in range(args.count):
try: try:
main(seed=seed, args=args) main(seed=seed, args=args, fish=fish)
logger.info('Finished run %s', _+1) logger.info('%s %s', fish.translate("cli","cli","finished.run"), _+1)
except (FillError, Exception, RuntimeError) as err: except (FillError, Exception, RuntimeError) as err:
failures.append((err, seed)) failures.append((err, seed))
logger.warning('Generation failed: %s', err) logger.warning('%s: %s', fish.translate("cli","cli","generation.failed"), err)
seed = random.randint(0, 999999999) seed = random.randint(0, 999999999)
for fail in failures: for fail in failures:
logger.info('%s seed failed with: %s', fail[1], fail[0]) logger.info('%s seed failed with: %s', fail[1], fail[0])
@@ -66,7 +74,7 @@ def start():
logger.info('Generation fail rate: ' + str(fail_rate[0] ).rjust(3, " ") + '.' + str(fail_rate[1] ).ljust(6, '0') + '%') 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') + '%') logger.info('Generation success rate: ' + str(success_rate[0]).rjust(3, " ") + '.' + str(success_rate[1]).ljust(6, '0') + '%')
else: else:
main(seed=args.seed, args=args) main(seed=args.seed, args=args, fish=fish)
if __name__ == '__main__': if __name__ == '__main__':

17
Gui.py
View File

@@ -21,6 +21,9 @@ from source.gui.bottom import bottom_frame, create_guiargs
from GuiUtils import set_icon from GuiUtils import set_icon
from Main import __version__ as ESVersion from Main import __version__ as ESVersion
from source.classes.BabelFish import BabelFish
from source.classes.Empty import Empty
def guiMain(args=None): def guiMain(args=None):
# Save settings to file # Save settings to file
@@ -143,14 +146,22 @@ def guiMain(args=None):
# add randomizer notebook to main window # add randomizer notebook to main window
self.pages["randomizer"].notebook.pack() self.pages["randomizer"].notebook.pack()
settings = get_args_priority(None, None, None)
lang = "en"
if "load" in settings and "lang" in settings["load"]:
lang = settings["load"]["lang"]
self.fish = BabelFish(lang=lang)
# bottom of window: Open Output Directory, Open Documentation (if exists) # bottom of window: Open Output Directory, Open Documentation (if exists)
self.frames["bottom"] = bottom_frame(self, self, None) self.pages["bottom"] = Empty()
self.pages["bottom"].pages = {}
self.pages["bottom"].pages["content"] = bottom_frame(self, self, None)
## Save Settings Button ## Save Settings Button
savesettingsButton = Button(self.frames["bottom"], text='Save Settings to File', command=lambda: save_settings_from_gui(True)) savesettingsButton = Button(self.pages["bottom"].pages["content"], text='Save Settings to File', command=lambda: save_settings_from_gui(True))
savesettingsButton.pack(side=RIGHT) savesettingsButton.pack(side=RIGHT)
# set bottom frame to main window # set bottom frame to main window
self.frames["bottom"].pack(side=BOTTOM, fill=X, padx=5, pady=5) self.pages["bottom"].pages["content"].pack(side=BOTTOM, fill=X, padx=5, pady=5)
self.outputPath = StringVar() self.outputPath = StringVar()
self.randomSprite = BooleanVar() self.randomSprite = BooleanVar()

62
Main.py
View File

@@ -27,7 +27,7 @@ from Utils import output_path, parse_player_names, print_wiki_doors_by_region, p
__version__ = '0.0.18.4d' __version__ = '0.0.18.4d'
def main(args, seed=None): def main(args, seed=None, fish=None):
if args.outputpath: if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True) os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath output_path.cached_path = args.outputpath
@@ -59,10 +59,15 @@ def main(args, seed=None):
world.beemizer = args.beemizer.copy() world.beemizer = args.beemizer.copy()
world.experimental = args.experimental.copy() world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
world.fish = fish
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
logger.info('ALttP Door Randomizer Version %s - Seed: %s\n', __version__, world.seed) logger.info(
world.fish.translate("cli","cli","app.title") + "\n",
__version__,
world.seed
)
parsed_names = parse_player_names(args.names, world.players, args.teams) parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names) world.teams = len(parsed_names)
@@ -95,7 +100,7 @@ def main(args, seed=None):
create_rooms(world, player) create_rooms(world, player)
create_dungeons(world, player) create_dungeons(world, player)
logger.info('Shuffling the World about.') logger.info(world.fish.translate("cli","cli","shuffling.world"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
@@ -103,7 +108,7 @@ def main(args, seed=None):
else: else:
link_inverted_entrances(world, player) link_inverted_entrances(world, player)
logger.info('Shuffling dungeons') logger.info(world.fish.translate("cli","cli","shuffling.dungeons"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
link_doors(world, player) link_doors(world, player)
@@ -111,21 +116,21 @@ def main(args, seed=None):
mark_light_world_regions(world, player) mark_light_world_regions(world, player)
else: else:
mark_dark_world_regions(world, player) mark_dark_world_regions(world, player)
logger.info('Generating Item Pool.') logger.info(world.fish.translate("cli","cli","generating.itempool"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
generate_itempool(world, player) generate_itempool(world, player)
logger.info('Calculating Access Rules.') logger.info(world.fish.translate("cli","cli","calc.access.rules"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
set_rules(world, player) set_rules(world, player)
logger.info('Placing Dungeon Prizes.') logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes"))
fill_prizes(world) fill_prizes(world)
logger.info('Placing Dungeon Items.') logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
shuffled_locations = None shuffled_locations = None
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
@@ -139,9 +144,17 @@ def main(args, seed=None):
for player in range(1, world.players+1): for player in range(1, world.players+1):
for key_layout in world.key_layout[player].values(): for key_layout in world.key_layout[player].values():
if not validate_key_placement(key_layout, world, player): if not validate_key_placement(key_layout, world, player):
raise RuntimeError("Keylock detected: %s (Player %d)" % (key_layout.sector.name, player)) raise RuntimeError(
"%s: %s (%s %d)" %
(
world.fish.translate("cli","cli","keylock.detected"),
key_layout.sector.name,
world.fish.translate("cli","cli","player"),
player
)
)
logger.info('Fill the world.') logger.info(world.fish.translate("cli","cli","fill.world"))
if args.algorithm == 'flood': if args.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items flood_items(world) # different algo, biased towards early game progress items
@@ -160,14 +173,14 @@ def main(args, seed=None):
distribute_items_restrictive(world, True) distribute_items_restrictive(world, True)
if world.players > 1: if world.players > 1:
logger.info('Balancing multiworld progression.') logger.info(world.fish.translate("cli","cli","balance.multiworld"))
balance_multiworld_progression(world) balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom # if we only check for beatable, we can do this sanity check first before creating the rom
if not world.can_beat_game(): if not world.can_beat_game():
raise RuntimeError('Cannot beat game. Something went terribly wrong here!') raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
logger.info('Patching ROM.') logger.info(world.fish.translate("cli","cli","patching.rom"))
outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed) outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed)
@@ -191,8 +204,8 @@ def main(args, seed=None):
if not args.jsonout: if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000) rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
else: else:
logging.warning("EnemizerCLI not found at:" + args.enemizercli) logging.warning(world.fish.translate("cli","cli","enemizer.not.found") + ': ' + args.enemizercli)
logging.warning("No Enemizer options will be applied until this is resolved.") logging.warning(world.fish.translate("cli","cli","enemizer.nothing.applied"))
if args.race: if args.race:
patch_race_rom(rom) patch_race_rom(rom)
@@ -246,7 +259,7 @@ def main(args, seed=None):
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
if not args.skip_playthrough: if not args.skip_playthrough:
logger.info('Calculating playthrough.') logger.info(world.fish.translate("cli","cli","calc.playthrough"))
create_playthrough(world) create_playthrough(world)
if args.jsonout: if args.jsonout:
@@ -254,8 +267,9 @@ def main(args, seed=None):
elif args.create_spoiler and not args.skip_playthrough: elif args.create_spoiler and not args.skip_playthrough:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Enjoy.') logger.info(world.fish.translate("cli","cli","done"))
logger.info('Total Time: %s', time.perf_counter() - start) logger.info(world.fish.translate("cli","cli","seed") + ": %d", world.seed)
logger.info(world.fish.translate("cli","cli","total.time"), time.perf_counter() - start)
# print_wiki_doors_by_room(dungeon_regions,world,1) # print_wiki_doors_by_room(dungeon_regions,world,1)
# print_wiki_doors_by_region(dungeon_regions,world,1) # print_wiki_doors_by_region(dungeon_regions,world,1)
@@ -402,7 +416,7 @@ def create_playthrough(world):
collection_spheres = [] collection_spheres = []
state = CollectionState(world) state = CollectionState(world)
sphere_candidates = list(prog_locations) sphere_candidates = list(prog_locations)
logging.getLogger('').debug('Building up collection spheres.') logging.getLogger('').debug(world.fish.translate("cli","cli","building.collection.spheres"))
while sphere_candidates: while sphere_candidates:
state.sweep_for_events(key_only=True) state.sweep_for_events(key_only=True)
state.sweep_for_crystal_access() state.sweep_for_crystal_access()
@@ -421,11 +435,11 @@ def create_playthrough(world):
state_cache.append(state.copy()) state_cache.append(state.copy())
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) logging.getLogger('').debug(world.fish.translate("cli","cli","building.calculating.spheres"), len(collection_spheres), len(sphere), len(prog_locations))
if not sphere: if not sphere:
logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) logging.getLogger('').debug(world.fish.translate("cli","cli","cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.') raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.progression"))
else: else:
old_world.spoiler.unreachables = sphere_candidates.copy() old_world.spoiler.unreachables = sphere_candidates.copy()
break break
@@ -477,9 +491,9 @@ def create_playthrough(world):
collection_spheres.append(sphere) collection_spheres.append(sphere)
logging.getLogger('').debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations))
if not sphere: if not sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
# store the required locations for statistical analysis # store the required locations for statistical analysis
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]

313
resources/app/cli/args.json Normal file
View File

@@ -0,0 +1,313 @@
{
"lang": {},
"create_spoiler": {
"action": "store_true",
"type": "bool"
},
"logic": {
"choices": [
"noglitches",
"minorglitches",
"nologic"
]
},
"mode": {
"choices": [
"open",
"standard",
"inverted",
"retro"
]
},
"swords": {
"choices": [
"random",
"assured",
"swordless",
"vanilla"
]
},
"goal": {
"choices": [
"ganon",
"pedestal",
"dungeons",
"triforcehunt",
"crystals"
]
},
"difficulty": {
"choices": [
"normal",
"hard",
"expert"
]
},
"item_functionality": {
"choices": [
"normal",
"hard",
"expert"
]
},
"timer": {
"choices": [
"none",
"display",
"timed",
"timed-ohko",
"ohko",
"timed-countdown"
]
},
"progressive": {
"choices": [
"on",
"off",
"random"
]
},
"algorithm": {
"choices": [
"balanced",
"freshness",
"flood",
"vt21",
"vt22",
"vt25",
"vt26"
]
},
"shuffle": {
"choices": [
"vanilla",
"simple",
"restricted",
"full",
"crossed",
"insanity",
"restricted_legacy",
"full_legacy",
"madness_legacy",
"insanity_legacy",
"dungeonsfull",
"dungeonssimple"
]
},
"door_shuffle": {
"choices": [
"basic",
"crossed",
"vanilla"
]
},
"experimental": {
"action": "store_true",
"type": "bool"
},
"dungeon_counters": {
"choices": [
"off",
"on",
"pickup",
"default"
]
},
"crystals_ganon": {
"choices": [
7, 6, 5, 4, 3, 2, 1, 0, "random"
]
},
"crystals_gt": {
"choices": [
7, 6, 5, 4, 3, 2, 1, 0, "random"
]
},
"openpyramid": {
"action": "store_true",
"type": "bool"
},
"rom": {},
"loglevel": {
"choices": [
"info",
"error",
"warning",
"debug"
]
},
"fastmenu": {
"choices": [
"normal",
"instant",
"double",
"triple",
"quadruple",
"half"
]
},
"quickswap": {
"action": "store_true",
"type": "bool"
},
"disablemusic": {
"action": "store_true",
"type": "bool"
},
"mapshuffle": {
"action": "store_true",
"type": "bool"
},
"compassshuffle": {
"action": "store_true",
"type": "bool"
},
"keyshuffle": {
"action": "store_true",
"type": "bool"
},
"bigkeyshuffle": {
"action": "store_true",
"type": "bool"
},
"keysanity": {
"action": "store_true",
"type": "bool",
"help": "suppress"
},
"retro": {
"action": "store_true",
"type": "bool"
},
"startinventory": {},
"usestartinventory": {
"type": "bool"
},
"custom": {
"type": "bool",
"help": "suppress"
},
"accessibility": {
"choices": [
"items",
"locations",
"none"
]
},
"hints": {
"action": "store_true",
"type": "bool"
},
"shuffleganon": {
"action": "store_true",
"type": "bool",
"help": "suppress"
},
"no-shuffleganon": {
"action": "store_false",
"dest": "shuffleganon"
},
"heartbeep": {
"choices": [
"normal",
"double",
"half",
"quarter",
"off"
]
},
"heartcolor": {
"choices": [
"red",
"blue",
"green",
"yellow",
"random"
]
},
"ow_palettes": {
"choices": [
"default",
"random",
"blackout"
]
},
"uw_palettes": {
"choices": [
"default",
"random",
"blackout"
]
},
"sprite": {},
"suppress_rom": {
"action": "store_true",
"type": "bool"
},
"gui": {
"action": "store_true"
},
"jsonout": {
"action": "store_true"
},
"skip_playthrough": {
"action": "store_true",
"type": "bool"
},
"enemizercli": {
"setting": "enemizercli"
},
"shufflebosses": {
"choices": [
"none",
"basic",
"normal",
"chaos"
]
},
"shuffleenemies": {
"choices": [
"none",
"shuffled",
"chaos"
]
},
"enemy_health": {
"choices": [
"default",
"easy",
"normal",
"hard",
"expert"
]
},
"enemy_damage": {
"choices": [
"default",
"shuffled",
"chaos"
]
},
"shufflepots": {
"action": "store_true",
"type": "bool"
},
"remote_items": {
"action": "store_true",
"type": "bool"
},
"names": {},
"outputpath": {},
"race": {
"action": "store_true",
"type": "bool"
},
"saveonexit": {
"choices": [
"ask",
"always",
"never"
]
},
"outputname": {}
}

View File

@@ -0,0 +1,27 @@
{
"cli": {
"app.title": "ALttP Tür Randomisier Version %s - Nummer: %d",
"shuffling.world": "Welt wird durchmischt.",
"generating.itempool": "Generier Gegenstandsbasis.",
"calc.access.rules": "Berechne Zugriffsregeln.",
"placing.dungeon.prizes": "Platziere Verliespreise.",
"placing.dungeon.items": "Platziere Verliesgegenstände.",
"fill.world": "Fülle die Welt.",
"balance.multiworld": "Gleiche Multiwelt-Fortschritt aus.",
"patching.rom": "Patche ROM.",
"calc.playthrough": "Berechne Durschpiellösung.",
"done": "Fertig. Viel Spaß.",
"total.time": "Gesamtzeit: %s",
"building.collection.spheres": "Baue Sammelbereiche auf.",
"building.calculating.spheres": "Berechneter Bereich %i, beinhaltet %i von %i Progressionsgegenständen.",
"cannot.reach.items": "Die folgenden Gegenstände können nicht erreicht werden: %s",
"cannot.reach.item": "%s (Spieler %d) in %s (Spieler %d)",
"check.item.location": "Prüfe ob %s (Spieler %d) benötigt wird um das Spiel zu schlagen.",
"check.item.location.true": "Ja, Gegenstand wird benötigt um das Spiel zu schlagen.",
"check.item.location.false": "Nein, Gegenstand wird nicht benötigt um das Spiel zu schlagen.",
"building.final.spheres": "Berechneter Finalbereich %i, beinhaltet, %i von %i Progressionsgegenständen.",
"cannot.beat.game": "Spiel is nicht schlagbar.",
"cannot.reach.progression": "Nicht alle Progressionsgegenstände erreichbar.",
"cannot.reach.required": "Nitch alle benötigten Gegenstände erreichbar."
}
}

View File

@@ -0,0 +1,272 @@
{
"cli": {
"app.title": "ALttP Door Randomizer Version %s - Seed: %d",
"version": "Version",
"seed": "Seed",
"player": "Player",
"shuffling.world": "Shuffling the World about",
"shuffling.dungeons": "Shuffling dungeons",
"basic.traversal": "--Basic Traversal",
"generating.dungeon": "Generating dungeon",
"shuffling.keydoors": "Shuffling Key doors for",
"lowering.keys.candidates": "Lowering key door count because not enough candidates",
"lowering.keys.layouts": "Lowering key door count because no valid layouts",
"keydoor.shuffle.time": "Key door shuffle time",
"keydoor.shuffle.time.crossed": "Cross Dungeon: Key door shuffle time",
"generating.itempool": "Generating Item Pool",
"calc.access.rules": "Calculating Access Rules",
"placing.dungeon.prizes": "Placing Dungeon Prizes",
"placing.dungeon.items": "Placing Dungeon Items",
"keylock.detected": "Keylock detected",
"fill.world": "Fill the world",
"balance.doors": "-Balancing Doors",
"re-balancing": "-Re-balancing",
"balancing": "--Balancing",
"splitting.up": "Splitting Up",
"balance.multiworld": "Balancing multiworld progression",
"cannot.beat.game": "Cannot beat game! Something went terribly wrong here!",
"cannot.reach.items": "The following items could not be reached: %s",
"cannot.reach.item": "%s (Player %d) at %s (Player %d)",
"check.item.location": "Checking if %s (Player %d) is required to beat the game.",
"check.item.location.true": "Yes, item is required.",
"check.item.location.false": "No, item is not required.",
"cannot.reach.progression": "Not all progression items reachable. Something went terribly wrong here.",
"cannot.reach.required": "Not all required items reachable. Something went terribly wrong here.",
"patching.rom": "Patching ROM",
"calc.playthrough": "Calculating playthrough",
"done": "Done. Enjoy.",
"total.time": "Total Time: %s",
"finished.run": "Finished run",
"generation.failed": "Generation failed",
"generation.fail.rate": "Generation fail rate",
"generation.success.rate": "Generation success rate",
"enemizer.not.found": "Enemizer not found at",
"enemizer.nothing.applied": "No Enemizer options will be applied until this is resolved.",
"building.collection.spheres": "Building up collection spheres",
"building.calculating.spheres": "Calculated sphere %i, containing %i of %i progress items.",
"building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items."
},
"help": {
"lang": [ "App Language, if available, defaults to English" ],
"create_spoiler": [ "Output a Spoiler File" ],
"logic": [
"Select Enforcement of Item Requirements. (default: %(default)s)",
"No Glitches: No Glitch knowledge required.",
"Minor Glitches: May require Fake Flippers, Bunny Revival",
" and Dark Room Navigation.",
"No Logic: Distribute items without regard for",
" item requirements."
],
"mode": [
"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.",
"Retro: Keys are universal, shooting arrows costs rupees,",
" and a few other little things make this more like Zelda-1."
],
"swords": [
"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."
],
"goal": [
"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."
],
"difficulty": [
"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."
],
"item_functionality": [
"Select limits on item functionality to increase difficulty. (default: %(default)s)",
"Normal: Normal functionality.",
"Hard: Reduced functionality.",
"Expert: Greatly reduced functionality."
],
"timer": [
"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)."
],
"progressive": [
"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."
],
"algorithm": [
"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."
],
"shuffle": [
"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."
],
"door_shuffle": [
"Select Door Shuffling Algorithm. (default: %(default)s)",
"Basic: Doors are mixed within a single dungeon.",
"Crossed: Doors are mixed between all dungeons.",
"Vanilla: All doors are connected the same way they were in the",
" base game."
],
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
"crystals_ganon": [
"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"
],
"crystals_gt": [
"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"
],
"openpyramid": [ "Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it. (default: %(default)s)" ],
"rom": [
"Path to an ALttP JP (1.0) rom to use as a base." ,
"(default: %(default)s)"
],
"loglevel": [ "Select level of logging for output. (default: %(default)s)" ],
"seed": [ "Define seed number to generate." ],
"count": [
"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 %(default)s seed(s) with",
"--seed given will produce the same %(default)s (different) rom(s) each",
"time)."
],
"fastmenu": [
"Select the rate at which the menu opens and closes. (default: %(default)s)"
],
"quickswap": [ "Enable quick item swapping with L and R. (default: %(default)s)" ],
"disablemusic": [ "Disables game music including MSU-1. (default: %(default)s)" ],
"mapshuffle": [ "Maps are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"compassshuffle": [ "Compasses are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ],
"retro": [
"Keys are universal, shooting arrows costs rupees,",
"and a few other little things make this more like Zelda-1. (default: %(default)s)"
],
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
"custom": [ "Not supported." ],
"customitemarray": [ "Not supported." ],
"accessibility": [
"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."
],
"hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ],
"no-shuffleganon": [
"If set, the Pyramid Hole and Ganon's Tower are not",
"included entrance shuffle pool. (default: %(default)s)"
],
"heartbeep": [
"Select the rate at which the heart beep sound is played at",
"low health. (default: %(default)s)"
],
"heartcolor": [ "Select the color of Link\\'s heart meter. (default: %(default)s)" ],
"sprite": [
"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."
],
"suppress_rom": [ "Do not create an output rom file. (default: %(default)s)" ],
"gui": [ "Launch the GUI. (default: %(default)s)" ],
"jsonout": [
"Output .json patch to stdout instead of a patched rom. Used",
"for VT site integration, do not use otherwise. (default: %(default)s)"
]
}
}

View File

@@ -0,0 +1,34 @@
{
"cli": {
"app.title": "ALttP Puerta Aleatorizador Versión %s - Número: %d",
"player": "Jugador",
"shuffling.world": "Barajando el Mundo",
"shuffling.dungeons": "Barajando Mazmorras",
"basic.traversal": "--Recorrido Básico",
"generating.dungeon": "Generando mazmorra",
"shuffling.keydoors": "Barajando Puertas Clave para",
"keylock.detected": "Bloqueo de Teclas detectado",
"fill.world": "Llenar el Mundo",
"balance.doors": "-Equilibriando Puertas",
"re-balancing": "-Reequilibriando",
"balancing": "--Equilibriando",
"splitting.up": "División",
"cannot.beat.game": "No se puede vencer el juego. Algo salió terriblemente mal.",
"cannot.reach.items": "No se pudo llegar a los siguientes elementos: %s",
"cannot.reach.item": "%s (Jugador %d) at %s (Jugador %d)",
"check.item.location": "Comprobar si se requiere que %s (Jugador %d) gane el juego.",
"check.item.location.true": "Sí, se requiere artículo.",
"check.item.location.false": "No, no se requiere artículo.",
"patching.rom": "Parchear ROM",
"calc.playthrough": "Cálculo de Juego",
"generation.failed": "Generación Fallida",
"enemizer.not.found": "Enemizer no encontrado en",
"building.collection.spheres": "Construyendo esferas de recolección.",
"building.calculating.spheres": "Esfera calculada %i, que contiene %i de %i elementos de progreso.",
"building.final.spheres": "Esfera final calculada %i, que contiene %i de %i elementos de progreso."
}
}

View File

@@ -0,0 +1,272 @@
{
"gui": {
"adjust.nobgm": "Disable Music & MSU-1",
"adjust.quickswap": "L/R Quickswapping",
"adjust.heartcolor": "Heart Color",
"adjust.heartcolor.red": "Red",
"adjust.heartcolor.blue": "Blue",
"adjust.heartcolor.green": "Green",
"adjust.heartcolor.yellow": "Yellow",
"adjust.heartcolor.random": "Random",
"adjust.heartbeep": "Heart Beep sound rate",
"adjust.heartbeep.double": "Double",
"adjust.heartbeep.normal": "Normal",
"adjust.heartbeep.half": "Half",
"adjust.heartbeep.quarter": "Quarter",
"adjust.heartbeep.off": "Off",
"adjust.menuspeed": "Menu Speed",
"adjust.menuspeed.instant": "Instant",
"adjust.menuspeed.quadruple": "Quadruple",
"adjust.menuspeed.triple": "Triple",
"adjust.menuspeed.double": "Double",
"adjust.menuspeed.normal": "Normal",
"adjust.menuspeed.half": "Half",
"adjust.owpalettes": "Overworld Palettes",
"adjust.owpalettes.default": "Default",
"adjust.owpalettes.random": "Random",
"adjust.owpalettes.blackout": "Blackout",
"adjust.uwpalettes": "Underworld Palettes",
"adjust.uwpalettes.default": "Default",
"adjust.uwpalettes.random": "Random",
"adjust.uwpalettes.blackout": "Blackout",
"adjust.sprite": "Sprite:",
"adjust.sprite.unchanged": "(unchanged)",
"adjust.rom": "Rom to adjust: ",
"adjust.rom.romfiles": "Rom Files",
"adjust.rom.button": "Select Rom",
"adjust.rom.go": "Adjust Rom",
"adjust.rom.dialog.error": "Error while patching",
"adjust.rom.dialog.success": "Success",
"adjust.rom.dialog.success.message": "Rom patched successfully.",
"randomizer.dungeon.keysanity": "Shuffle: ",
"randomizer.dungeon.mapshuffle": "Maps",
"randomizer.dungeon.compassshuffle": "Compasses",
"randomizer.dungeon.smallkeyshuffle": "Small Keys",
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
"randomizer.dungeon.dungeondoorshuffle.basic": "Basic",
"randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed",
"randomizer.dungeon.experimental": "Enable Experimental Features",
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
"randomizer.dungeon.dungeon_counters.default": "Auto",
"randomizer.dungeon.dungeon_counters.off": "Off",
"randomizer.dungeon.dungeon_counters.on": "On",
"randomizer.dungeon.dungeon_counters.pickup": "On Compass Pickup",
"randomizer.enemizer.potshuffle": "Pot Shuffle",
"randomizer.enemizer.enemyshuffle": "Enemy Shuffle",
"randomizer.enemizer.enemyshuffle.none": "Vanilla",
"randomizer.enemizer.enemyshuffle.shuffled": "Shuffled",
"randomizer.enemizer.enemyshuffle.chaos": "Chaos",
"randomizer.enemizer.bossshuffle": "Boss Shuffle",
"randomizer.enemizer.bossshuffle.none": "Vanilla",
"randomizer.enemizer.bossshuffle.basic": "Basic",
"randomizer.enemizer.bossshuffle.shuffled": "Shuffled",
"randomizer.enemizer.bossshuffle.chaos": "Chaos",
"randomizer.enemizer.enemydamage": "Enemy Damage",
"randomizer.enemizer.enemydamage.default": "Vanilla",
"randomizer.enemizer.enemydamage.shuffled": "Shuffled",
"randomizer.enemizer.enemydamage.chaos": "Chaos",
"randomizer.enemizer.enemyhealth": "Enemy Health",
"randomizer.enemizer.enemyhealth.default": "Vanilla",
"randomizer.enemizer.enemyhealth.easy": "Easy",
"randomizer.enemizer.enemyhealth.normal": "Normal",
"randomizer.enemizer.enemyhealth.hard": "Hard",
"randomizer.enemizer.enemyhealth.expert": "Expert",
"randomizer.enemizer.enemizercli": "EnemizerCLI path: ",
"randomizer.enemizer.enemizercli.online": "(get online)",
"randomizer.entrance.openpyramid": "Pre-open Pyramid Hole",
"randomizer.entrance.shuffleganon": "Include Ganon's Tower and Pyramid Hole in shuffle pool",
"randomizer.entrance.entranceshuffle": "Entrance Shuffle",
"randomizer.entrance.entranceshuffle.vanilla": "Vanilla",
"randomizer.entrance.entranceshuffle.simple": "Simple",
"randomizer.entrance.entranceshuffle.restricted": "Restricted",
"randomizer.entrance.entranceshuffle.full": "Full",
"randomizer.entrance.entranceshuffle.crossed": "Crossed",
"randomizer.entrance.entranceshuffle.insanity": "Insanity",
"randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)",
"randomizer.entrance.entranceshuffle.full_legacy": "Full (Legacy)",
"randomizer.entrance.entranceshuffle.madness_legacy": "Madness (Legacy)",
"randomizer.entrance.entranceshuffle.insanity_legacy": "Insanity (Legacy)",
"randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full",
"randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple",
"randomizer.gameoptions.hints": "Include Helpful Hints",
"randomizer.gameoptions.nobgm": "Disable Music & MSU-1",
"randomizer.gameoptions.quickswap": "L/R Quickswapping",
"randomizer.gameoptions.heartcolor": "Heart Color",
"randomizer.gameoptions.heartcolor.red": "Red",
"randomizer.gameoptions.heartcolor.blue": "Blue",
"randomizer.gameoptions.heartcolor.green": "Green",
"randomizer.gameoptions.heartcolor.yellow": "Yellow",
"randomizer.gameoptions.heartcolor.random": "Random",
"randomizer.gameoptions.heartbeep": "Heart Beep sound rate",
"randomizer.gameoptions.heartbeep.double": "Double",
"randomizer.gameoptions.heartbeep.normal": "Normal",
"randomizer.gameoptions.heartbeep.half": "Half",
"randomizer.gameoptions.heartbeep.quarter": "Quarter",
"randomizer.gameoptions.heartbeep.off": "Off",
"randomizer.gameoptions.menuspeed": "Menu Speed",
"randomizer.gameoptions.menuspeed.instant": "Instant",
"randomizer.gameoptions.menuspeed.quadruple": "Quadruple",
"randomizer.gameoptions.menuspeed.triple": "Triple",
"randomizer.gameoptions.menuspeed.double": "Double",
"randomizer.gameoptions.menuspeed.normal": "Normal",
"randomizer.gameoptions.menuspeed.half": "Half",
"randomizer.gameoptions.owpalettes": "Overworld Palettes",
"randomizer.gameoptions.owpalettes.default": "Default",
"randomizer.gameoptions.owpalettes.random": "Random",
"randomizer.gameoptions.owpalettes.blackout": "Blackout",
"randomizer.gameoptions.uwpalettes": "Underworld Palettes",
"randomizer.gameoptions.uwpalettes.default": "Default",
"randomizer.gameoptions.uwpalettes.random": "Random",
"randomizer.gameoptions.uwpalettes.blackout": "Blackout",
"randomizer.gameoptions.sprite": "Sprite:",
"randomizer.gameoptions.sprite.unchanged": "(unchanged)",
"randomizer.generation.spoiler": "Create Spoiler Log",
"randomizer.generation.suppressrom": "Do not create patched ROM",
"randomizer.generation.usestartinventory": "Use starting inventory",
"randomizer.generation.usecustompool": "Use custom item pool",
"randomizer.generation.saveonexit": "Save Settings on Exit",
"randomizer.generation.saveonexit.ask": "Ask Me",
"randomizer.generation.saveonexit.always": "Always",
"randomizer.generation.saveonexit.never": "Never",
"randomizer.generation.rom": "Base Rom: ",
"randomizer.generation.rom.button": "Select Rom",
"randomizer.generation.rom.dialog.romfiles": "Rom Files",
"randomizer.generation.rom.dialog.allfiles": "All Files",
"randomizer.item.retro": "Retro mode (universal keys)",
"randomizer.item.worldstate": "World State",
"randomizer.item.worldstate.standard": "Standard",
"randomizer.item.worldstate.open": "Open",
"randomizer.item.worldstate.inverted": "Inverted",
"randomizer.item.worldstate.retro": "Retro",
"randomizer.item.logiclevel": "Logic Level",
"randomizer.item.logiclevel.noglitches": "No Glitches",
"randomizer.item.logiclevel.minorglitches": "Minor Glitches",
"randomizer.item.logiclevel.nologic": "No Logic",
"randomizer.item.goal": "Goal",
"randomizer.item.goal.ganon": "Defeat Ganon",
"randomizer.item.goal.pedestal": "Master Sword Pedestal",
"randomizer.item.goal.dungeons": "All Dungeons",
"randomizer.item.goal.triforcehunt": "Triforce Hunt",
"randomizer.item.goal.crystals": "Crystals",
"randomizer.item.crystals_gt": "Crystals to open GT",
"randomizer.item.crystals_gt.0": "0",
"randomizer.item.crystals_gt.1": "1",
"randomizer.item.crystals_gt.2": "2",
"randomizer.item.crystals_gt.3": "3",
"randomizer.item.crystals_gt.4": "4",
"randomizer.item.crystals_gt.5": "5",
"randomizer.item.crystals_gt.6": "6",
"randomizer.item.crystals_gt.7": "7",
"randomizer.item.crystals_gt.random": "Random",
"randomizer.item.crystals_ganon": "Crystals to harm Ganon",
"randomizer.item.crystals_ganon.0": "0",
"randomizer.item.crystals_ganon.1": "1",
"randomizer.item.crystals_ganon.2": "2",
"randomizer.item.crystals_ganon.3": "3",
"randomizer.item.crystals_ganon.4": "4",
"randomizer.item.crystals_ganon.5": "5",
"randomizer.item.crystals_ganon.6": "6",
"randomizer.item.crystals_ganon.7": "7",
"randomizer.item.crystals_ganon.random": "Random",
"randomizer.item.weapons": "Weapons",
"randomizer.item.weapons.random": "Randomized",
"randomizer.item.weapons.assured": "Assured",
"randomizer.item.weapons.swordless": "Swordless",
"randomizer.item.weapons.vanilla": "Vanilla",
"randomizer.item.itempool": "Item Pool",
"randomizer.item.itempool.normal": "Normal",
"randomizer.item.itempool.hard": "Hard",
"randomizer.item.itempool.expert": "Expert",
"randomizer.item.itemfunction": "Item Functionality",
"randomizer.item.itemfunction.normal": "Normal",
"randomizer.item.itemfunction.hard": "Hard",
"randomizer.item.itemfunction.expert": "Expert",
"randomizer.item.timer": "Timer Setting",
"randomizer.item.timer.none": "No Timer",
"randomizer.item.timer.display": "Stopwatch",
"randomizer.item.timer.timed": "Timed",
"randomizer.item.timer.timed-ohko": "Timed OHKO",
"randomizer.item.timer.ohko": "OHKO",
"randomizer.item.timer.timed-countdown": "Timed Countdown",
"randomizer.item.progressives": "Progressive Items",
"randomizer.item.progressives.on": "On",
"randomizer.item.progressives.off": "Off",
"randomizer.item.progressives.random": "Random",
"randomizer.item.accessibility": "Accessibility",
"randomizer.item.accessibility.items": "100% Inventory",
"randomizer.item.accessibility.locations": "100% Locations",
"randomizer.item.accessibility.none": "Beatable",
"randomizer.item.sortingalgo": "Item Sorting",
"randomizer.item.sortingalgo.freshness": "Freshness",
"randomizer.item.sortingalgo.flood": "Flood",
"randomizer.item.sortingalgo.vt21": "VT8.21",
"randomizer.item.sortingalgo.vt22": "VT8.22",
"randomizer.item.sortingalgo.vt25": "VT8.25",
"randomizer.item.sortingalgo.vt26": "VT8.26",
"randomizer.item.sortingalgo.balanced": "Balanced",
"randomizer.multiworld.worlds": "Worlds",
"randomizer.multiworld.names": "Player names",
"bottom.content.seed": "Seed #",
"bottom.content.generationcount": "Count",
"bottom.content.go": "Generate Patched Rom",
"bottom.content.dialog.error": "Error while creating seed",
"bottom.content.dialog.success": "Success",
"bottom.content.dialog.success.message": "Rom created successfully.",
"bottom.content.outputdir": "Open Output Directory",
"bottom.content.docs": "Open Documentation"
}
}

View File

@@ -11,13 +11,13 @@
"selectbox": { "selectbox": {
"side": "right" "side": "right"
}, },
"default": "Basic" "default": "basic"
}, },
"options": { "options": [
"Vanilla": "vanilla", "vanilla",
"Basic": "basic", "basic",
"Crossed": "crossed" "crossed"
} ]
}, },
"experimental": { "experimental": {
"type": "checkbox", "type": "checkbox",
@@ -37,13 +37,13 @@
"selectbox": { "selectbox": {
"side": "right" "side": "right"
}, },
"default": "Auto" "default": "default"
}, },
"options": { "options": [
"Auto": "default", "default",
"Off": "off", "off",
"On": "on", "on",
"On Compass Pickup": "pickup" "pickup"
} ]
} }
} }

View File

@@ -21,11 +21,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Vanilla": "none", "none",
"Shuffled": "shuffled", "shuffled",
"Chaos": "chaos" "chaos"
} ]
}, },
"bossshuffle": { "bossshuffle": {
"type": "selectbox", "type": "selectbox",
@@ -40,12 +40,12 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Vanilla": "none", "none",
"Basic": "basic", "basic",
"Shuffled": "shuffled", "shuffled",
"Chaos": "chaos" "chaos"
} ]
} }
}, },
"rightEnemizerFrame": { "rightEnemizerFrame": {
@@ -62,11 +62,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Vanilla": "default", "default",
"Shuffled": "shuffled", "shuffled",
"Chaos": "chaos" "chaos"
} ]
}, },
"enemyhealth": { "enemyhealth": {
"type": "selectbox", "type": "selectbox",
@@ -81,13 +81,13 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Vanilla": "default", "default",
"Easy": "easy", "easy",
"Normal": "normal", "normal",
"Hard": "hard", "hard",
"Expert": "expert" "expert"
} ]
} }
} }
} }

View File

@@ -21,20 +21,20 @@
"label": { "side": "left" }, "label": { "side": "left" },
"selectbox": { "side": "right" } "selectbox": { "side": "right" }
}, },
"options": { "options": [
"Vanilla": "vanilla", "vanilla",
"Simple": "simple", "simple",
"Restricted": "restricted", "restricted",
"Full": "full", "full",
"Crossed": "crossed", "crossed",
"Insanity": "insanity", "insanity",
"Restricted (Legacy)": "restricted_legacy", "restricted_legacy",
"Full (Legacy)": "full_legacy", "full_legacy",
"Madness (Legacy)": "madness_legacy", "madness_legacy",
"Insanity (Legacy)": "insanity_legacy", "insanity_legacy",
"Dungeons + Full": "dungeonsfull", "dungeonsfull",
"Dungeons + Simple": "dungeonssimple" "dungeonssimple"
} ]
} }
} }
} }

View File

@@ -34,13 +34,13 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Red": "red", "red",
"Blue": "blue", "blue",
"Green": "green", "green",
"Yellow": "yellow", "yellow",
"Random": "random" "random"
} ]
}, },
"heartbeep": { "heartbeep": {
"type": "selectbox", "type": "selectbox",
@@ -54,15 +54,15 @@
"selectbox": { "selectbox": {
"side": "right" "side": "right"
}, },
"default": "Normal" "default": "normal"
}, },
"options": { "options": [
"Double": "double", "double",
"Normal": "normal", "normal",
"Half": "half", "half",
"Quarter": "quarter", "quarter",
"Off": "off" "off"
} ]
} }
}, },
"rightRomOptionsFrame": { "rightRomOptionsFrame": {
@@ -78,16 +78,16 @@
"selectbox": { "selectbox": {
"side": "right" "side": "right"
}, },
"default": "Normal" "default": "normal"
}, },
"options": { "options": [
"Instant": "instant", "instant",
"Quadruple": "quadruple", "quadruple",
"Triple": "triple", "triple",
"Double": "double", "double",
"Normal": "normal", "normal",
"Half": "half" "half"
} ]
}, },
"owpalettes": { "owpalettes": {
"type": "selectbox", "type": "selectbox",
@@ -102,11 +102,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Default": "default", "default",
"Random": "random", "random",
"Blackout": "blackout" "blackout"
} ]
}, },
"uwpalettes": { "uwpalettes": {
"type": "selectbox", "type": "selectbox",
@@ -121,11 +121,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Default": "default", "default",
"Random": "random", "random",
"Blackout": "blackout" "blackout"
} ]
} }
} }
} }

View File

@@ -36,10 +36,10 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Ask Me": "ask", "ask",
"Always": "always", "always",
"Never": "never" "never"
} ]
} }
} }

View File

@@ -20,14 +20,14 @@
"selectbox": { "selectbox": {
"side": "right" "side": "right"
}, },
"default": "Open" "default": "open"
}, },
"options": { "options": [
"Standard": "standard", "standard",
"Open": "open", "open",
"Inverted": "inverted", "inverted",
"Retro": "retro" "retro"
} ]
}, },
"logiclevel": { "logiclevel": {
"type": "selectbox", "type": "selectbox",
@@ -42,11 +42,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"No Glitches": "noglitches", "noglitches",
"Minor Glitches": "minorglitches", "minorglitches",
"No Logic": "nologic" "nologic"
} ]
}, },
"goal": { "goal": {
"type": "selectbox", "type": "selectbox",
@@ -61,13 +61,13 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Defeat Ganon": "ganon", "ganon",
"Master Sword Pedestal": "pedestal", "pedestal",
"All Dungeons": "dungeons", "dungeons",
"Triforce Hunt": "triforcehunt", "triforcehunt",
"Crystals": "crystals" "crystals"
} ]
}, },
"crystals_gt": { "crystals_gt": {
"type": "selectbox", "type": "selectbox",
@@ -82,17 +82,17 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"0": "0", "0",
"1": "1", "1",
"2": "2", "2",
"3": "3", "3",
"4": "4", "4",
"5": "5", "5",
"6": "6", "6",
"7": "7", "7",
"Random": "random" "random"
} ]
}, },
"crystals_ganon": { "crystals_ganon": {
"type": "selectbox", "type": "selectbox",
@@ -107,17 +107,17 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"0": "0", "0",
"1": "1", "1",
"2": "2", "2",
"3": "3", "3",
"4": "4", "4",
"5": "5", "5",
"6": "6", "6",
"7": "7", "7",
"Random": "random" "random"
} ]
}, },
"weapons": { "weapons": {
"type": "selectbox", "type": "selectbox",
@@ -132,12 +132,12 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Randomized": "random", "random",
"Assured": "assured", "assured",
"Swordless": "swordless", "swordless",
"Vanilla": "vanilla" "vanilla"
} ]
} }
}, },
"rightItemFrame": { "rightItemFrame": {
@@ -154,11 +154,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Normal": "normal", "normal",
"Hard": "hard", "hard",
"Expert": "expert" "expert"
} ]
}, },
"itemfunction": { "itemfunction": {
"type": "selectbox", "type": "selectbox",
@@ -173,11 +173,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"Normal": "normal", "normal",
"Hard": "hard", "hard",
"Expert": "expert" "expert"
} ]
}, },
"timer": { "timer": {
"type": "selectbox", "type": "selectbox",
@@ -192,14 +192,14 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"No Timer": "none", "none",
"Stopwatch": "display", "display",
"Timed": "timed", "timed",
"Timed OHKO": "timed-ohko", "timed-ohko",
"OHKO": "ohko", "ohko",
"Timed Countdown": "timed-countdown" "timed-countdown"
} ]
}, },
"progressives": { "progressives": {
"type": "selectbox", "type": "selectbox",
@@ -214,11 +214,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"On": "on", "on",
"Off": "off", "off",
"Random": "random" "random"
} ]
}, },
"accessibility": { "accessibility": {
"type": "selectbox", "type": "selectbox",
@@ -233,11 +233,11 @@
"side": "right" "side": "right"
} }
}, },
"options": { "options": [
"100% Inventory": "items", "items",
"100% Locations": "locations", "locations",
"Beatable": "none" "none"
} ]
}, },
"sortingalgo": { "sortingalgo": {
"type": "selectbox", "type": "selectbox",
@@ -251,17 +251,17 @@
"selectbox": { "selectbox": {
"side": "right" "side": "right"
}, },
"default": "Balanced" "default": "balanced"
}, },
"options": { "options": [
"Freshness": "freshness", "freshness",
"Flood": "flood", "flood",
"VT8.21": "vt21", "vt21",
"VT8.22": "vt22", "vt22",
"VT8.25": "vt25", "vt25",
"VT8.26": "vt26", "vt26",
"Balanced": "balanced" "balanced"
} ]
} }
} }
} }

View File

@@ -0,0 +1,3 @@
{
}

112
source/classes/BabelFish.py Normal file
View File

@@ -0,0 +1,112 @@
import json
import locale
import os
class BabelFish():
def __init__(self,subpath=["resources","app","meta"],lang=None):
localization_string = locale.getdefaultlocale()[0] #get set localization
self.locale = localization_string[:2] if lang is None else lang #let caller override localization
self.langs = ["en"] #start with English
if(not self.locale == "en"): #add localization
self.langs.append(self.locale)
self.lang_defns = {} #collect translations
self.add_translation_file() #start with default translation file
self.add_translation_file(["resources","app","cli"]) #add help translation file
self.add_translation_file(["resources","app","gui"]) #add gui label translation file
self.add_translation_file(["resources","user","meta"]) #add user translation file
def add_translation_file(self,subpath=["resources","app","meta"]):
if not isinstance(subpath, list):
subpath = [subpath]
if "lang" not in subpath:
subpath.append("lang") #look in lang folder
subpath = os.path.join(*subpath) #put in path separators
key = subpath.split(os.sep)
for check in ["resources","app","user"]:
if check in key:
key.remove(check)
key = os.path.join(*key) #put in path separators
for lang in self.langs:
if not lang in self.lang_defns:
self.lang_defns[lang] = {}
langs_filename = os.path.join(subpath,lang + ".json") #get filename of translation file
if os.path.isfile(langs_filename): #if we've got a file
with open(langs_filename,encoding="utf-8") as f: #open it
self.lang_defns[lang][key[:key.rfind(os.sep)].replace(os.sep,'.')] = json.load(f) #save translation definitions
else:
pass
# print(langs_filename + " not found for translation!")
def translate(self, domain="", key="", subkey=""): #three levels of keys
# start with nothing
display_text = ""
# exits check for not exit first and then append Exit at end
# multiRooms check for not chest name first and then append chest name at end
specials = {
"exit": False,
"multiRoom": False
}
# Domain
if os.sep in domain:
domain = domain.replace(os.sep,'.')
# display_text = domain
# Operate on Key
if key != "":
if display_text != "":
display_text += '.'
# display_text += key
# Exits
if "exit" in key and "gui" not in domain:
key = key.replace("exit","")
specials["exit"] = True
if "Exit" in key and "gui" not in domain:
key = key.replace("Exit","")
specials["exit"] = True
# Locations
tmp = key.split(" - ")
if len(tmp) >= 2:
specials["multiRoom"] = tmp[len(tmp) - 1]
tmp.pop()
key = " - ".join(tmp)
key = key.strip()
# Operate on Subkey
if subkey != "":
if display_text != "":
display_text += '.'
display_text += subkey
# Exits
if "exit" in subkey and "gui" not in domain:
subkey = subkey.replace("exit","")
specials["exit"] = True
if "Exit" in subkey and "gui" not in domain:
subkey = subkey.replace("Exit","")
specials["exit"] = True
# Locations
tmp = subkey.split(" - ")
if len(tmp) >= 2:
specials["multiRoom"] = tmp[len(tmp) - 1]
tmp.pop()
subkey = " - ".join(tmp)
subkey = subkey.strip()
my_lang = self.lang_defns[self.locale] #handle for localization
en_lang = self.lang_defns["en"] #handle for English
if domain in my_lang and key in my_lang[domain] and subkey in my_lang[domain][key] and not my_lang[domain][key][subkey] == "": #get localization first
display_text = my_lang[domain][key][subkey]
elif domain in en_lang and key in en_lang[domain] and subkey in en_lang[domain][key] and not en_lang[domain][key][subkey] == "": #gracefully degrade to English
display_text = en_lang[domain][key][subkey]
elif specials["exit"]:
specials["exit"] = False
if specials["exit"]:
display_text += " Exit"
elif specials["multiRoom"] and specials["multiRoom"] not in display_text:
display_text += " - " + specials["multiRoom"]
return display_text

3
source/classes/Empty.py Normal file
View File

@@ -0,0 +1,3 @@
# Need a dummy class
class Empty():
pass

View File

@@ -110,5 +110,11 @@ SETTINGSTOPROCESS = {
"usecustompool": "custom", "usecustompool": "custom",
"saveonexit": "saveonexit" "saveonexit": "saveonexit"
} }
},
"bottom": {
"content": {
"seed": "seed",
"generationcount": "count"
}
} }
} }

View File

@@ -8,6 +8,7 @@ from Main import main
from Utils import local_path, output_path, open_file from Utils import local_path, output_path, open_file
import source.classes.constants as CONST import source.classes.constants as CONST
import source.gui.widgets as widgets import source.gui.widgets as widgets
from source.classes.Empty import Empty
def bottom_frame(self, parent, args=None): def bottom_frame(self, parent, args=None):
@@ -17,19 +18,34 @@ def bottom_frame(self, parent, args=None):
# Bottom Frame options # Bottom Frame options
self.widgets = {} self.widgets = {}
seedCountFrame = Frame(self) # Seed input
seedCountFrame.pack() # widget ID
## Seed # widget = "seed"
seedLabel = Label(self, text='Seed #')
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# frame
self.widgets[widget].pieces["frame"] = Frame(self)
# frame: label
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text="Seed #")
self.widgets[widget].pieces["frame"].label.pack(side=LEFT)
# storagevar
savedSeed = parent.settings["seed"] savedSeed = parent.settings["seed"]
self.seedVar = StringVar(value=savedSeed) self.widgets[widget].storageVar = StringVar(value=savedSeed)
# textbox
self.widgets[widget].type = "textbox"
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], width=15, textvariable=self.widgets[widget].storageVar)
self.widgets[widget].pieces["textbox"].pack(side=LEFT)
def saveSeed(caller,_,mode): def saveSeed(caller,_,mode):
savedSeed = self.seedVar.get() savedSeed = self.widgets["seed"].storageVar.get()
parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None
self.seedVar.trace_add("write",saveSeed) self.widgets[widget].storageVar.trace_add("write",saveSeed)
seedEntry = Entry(self, width=15, textvariable=self.seedVar) # frame: pack
seedLabel.pack(side=LEFT) self.widgets[widget].pieces["frame"].pack(side=LEFT)
seedEntry.pack(side=LEFT)
## Number of Generation attempts ## Number of Generation attempts
key = "generationcount" key = "generationcount"
@@ -56,10 +72,10 @@ def bottom_frame(self, parent, args=None):
if guiargs.count is not None: if guiargs.count is not None:
seed = guiargs.seed seed = guiargs.seed
for _ in range(guiargs.count): for _ in range(guiargs.count):
main(seed=seed, args=guiargs) main(seed=seed, args=guiargs, fish=parent.fish)
seed = random.randint(0, 999999999) seed = random.randint(0, 999999999)
else: else:
main(seed=guiargs.seed, args=guiargs) main(seed=guiargs.seed, args=guiargs, fish=parent.fish)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e)) messagebox.showerror(title="Error while creating seed", message=str(e))
@@ -67,8 +83,19 @@ def bottom_frame(self, parent, args=None):
messagebox.showinfo(title="Success", message="Rom patched successfully") messagebox.showinfo(title="Success", message="Rom patched successfully")
## Generate Button ## Generate Button
generateButton = Button(self, text='Generate Patched Rom', command=generateRom) # widget ID
generateButton.pack(side=LEFT) widget = "go"
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# button
self.widgets[widget].type = "button"
self.widgets[widget].pieces["button"] = Button(self, text='Generate Patched Rom', command=generateRom)
# button: pack
self.widgets[widget].pieces["button"].pack(side=LEFT)
def open_output(): def open_output():
if args and args.outputpath: if args and args.outputpath:
@@ -76,15 +103,42 @@ def bottom_frame(self, parent, args=None):
else: else:
open_file(output_path(parent.settings["outputpath"])) open_file(output_path(parent.settings["outputpath"]))
openOutputButton = Button(self, text='Open Output Directory', command=open_output) ## Output Button
openOutputButton.pack(side=RIGHT) # widget ID
widget = "outputdir"
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# storagevar
self.widgets[widget].storageVar = StringVar(value=parent.settings["outputpath"])
# button
self.widgets[widget].type = "button"
self.widgets[widget].pieces["button"] = Button(self, text='Open Output Directory', command=open_output)
# button: pack
self.widgets[widget].pieces["button"].pack(side=RIGHT)
## Documentation Button ## Documentation Button
# widget ID
widget = "docs"
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# button
self.widgets[widget].type = "button"
self.widgets[widget].selectbox = Empty()
self.widgets[widget].selectbox.storageVar = Empty()
if os.path.exists(local_path('README.html')): if os.path.exists(local_path('README.html')):
def open_readme(): def open_readme():
open_file(local_path('README.html')) open_file(local_path('README.html'))
openReadmeButton = Button(self, text='Open Documentation', command=open_readme) self.widgets[widget].pieces["button"] = Button(self, text='Open Documentation', command=open_readme)
openReadmeButton.pack(side=RIGHT) # button: pack
self.widgets[widget].pieces["button"].pack(side=RIGHT)
return self return self
@@ -107,22 +161,26 @@ def create_guiargs(parent):
setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get()) setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get())
# Get EnemizerCLI setting # Get EnemizerCLI setting
guiargs.enemizercli = parent.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.get() guiargs.enemizercli = parent.pages["randomizer"].pages["enemizer"].widgets["enemizercli"].storageVar.get()
# Get Multiworld Worlds count # Get Multiworld Worlds count
guiargs.multi = int(parent.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.get()) guiargs.multi = int(parent.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.get())
# Get baserom path # Get baserom path
guiargs.rom = parent.pages["randomizer"].pages["generation"].romVar.get() guiargs.rom = parent.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
# Get if we're using the Custom Item Pool # Get if we're using the Custom Item Pool
guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get()) guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get())
# Get Seed ID # Get Seed ID
guiargs.seed = int(parent.frames["bottom"].seedVar.get()) if parent.frames["bottom"].seedVar.get() else None guiargs.seed = None
if parent.pages["bottom"].pages["content"].widgets["seed"].storageVar.get():
guiargs.seed = parent.pages["bottom"].pages["content"].widgets["seed"].storageVar.get()
# Get number of generations to run # Get number of generations to run
guiargs.count = int(parent.frames["bottom"].widgets["generationcount"].storageVar.get()) if parent.frames["bottom"].widgets["generationcount"].storageVar.get() != '1' else None guiargs.count = 1
if parent.pages["bottom"].pages["content"].widgets["generationcount"].storageVar.get():
guiargs.count = int(parent.pages["bottom"].pages["content"].widgets["generationcount"].storageVar.get())
# Get Adjust settings # Get Adjust settings
adjustargs = { adjustargs = {

View File

@@ -2,10 +2,13 @@ from source.classes.SpriteSelector import SpriteSelector as spriteSelector
from source.gui.randomize.gameoptions import set_sprite from source.gui.randomize.gameoptions import set_sprite
from Rom import Sprite, get_sprite_from_name from Rom import Sprite, get_sprite_from_name
import source.classes.constants as CONST import source.classes.constants as CONST
from source.classes.BabelFish import BabelFish
from source.classes.Empty import Empty
# Load args/settings for most tabs # Load args/settings for most tabs
def loadcliargs(gui, args, settings=None): def loadcliargs(gui, args, settings=None):
if args is not None: if args is not None:
fish = BabelFish()
# for k, v in vars(args).items(): # for k, v in vars(args).items():
# if type(v) is dict: # if type(v) is dict:
# setattr(args, k, v[1]) # only get values for player 1 for now # setattr(args, k, v[1]) # only get values for player 1 for now
@@ -21,39 +24,146 @@ def loadcliargs(gui, args, settings=None):
for subpage in options[mainpage]: for subpage in options[mainpage]:
# Cycle through each widget # Cycle through each widget
for widget in options[mainpage][subpage]: for widget in options[mainpage][subpage]:
# Get the value and set it if widget in gui.pages[mainpage].pages[subpage].widgets:
arg = options[mainpage][subpage][widget] thisType = ""
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg]) # Get the value and set it
# If we're on the Game Options page and it's not about Hints arg = options[mainpage][subpage][widget]
if subpage == "gameoptions" and not widget == "hints": if args[arg] == None:
# Check if we've got settings args[arg] = ""
# Check if we've got the widget in Adjust settings label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
hasSettings = settings is not None if hasattr(gui.pages[mainpage].pages[subpage].widgets[widget],"type"):
hasWidget = ("adjust." + widget) in settings if hasSettings else None thisType = gui.pages[mainpage].pages[subpage].widgets[widget].type
if hasWidget is None: if thisType == "checkbox":
# If we've got a Game Options val and we don't have an Adjust val, use the Game Options val gui.pages[mainpage].pages[subpage].widgets[widget].checkbox.configure(text=label)
gui.pages["adjust"].content.widgets[widget].storageVar.set(args[arg]) elif thisType == "selectbox":
theseOptions = gui.pages[mainpage].pages[subpage].widgets[widget].selectbox.options
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
i = 0
for value in theseOptions["values"]:
gui.pages[mainpage].pages[subpage].widgets[widget].selectbox.options["labels"][i] = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget + '.' + value)
i += 1
for i in range(0, len(theseOptions["values"])):
gui.pages[mainpage].pages[subpage].widgets[widget].selectbox["menu"].entryconfigure(i, label=theseOptions["labels"][i])
gui.pages[mainpage].pages[subpage].widgets[widget].selectbox.options = theseOptions
elif thisType == "spinbox":
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg])
# If we're on the Game Options page and it's not about Hints
if subpage == "gameoptions" and not widget == "hints":
# Check if we've got settings
# Check if we've got the widget in Adjust settings
hasSettings = settings is not None
hasWidget = ("adjust." + widget) in settings if hasSettings else None
label = fish.translate("gui","gui","adjust." + widget)
if ("adjust." + widget) in label:
label = fish.translate("gui","gui","randomizer.gameoptions." + widget)
if hasattr(gui.pages["adjust"].content.widgets[widget],"type"):
type = gui.pages["adjust"].content.widgets[widget].type
if type == "checkbox":
gui.pages["adjust"].content.widgets[widget].checkbox.configure(text=label)
elif type == "selectbox":
gui.pages["adjust"].content.widgets[widget].label.configure(text=label)
if hasWidget is None:
# If we've got a Game Options val and we don't have an Adjust val, use the Game Options val
gui.pages["adjust"].content.widgets[widget].storageVar.set(args[arg])
# Get EnemizerCLI setting # Get EnemizerCLI setting
gui.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.set(args["enemizercli"]) mainpage = "randomizer"
subpage = "enemizer"
widget = "enemizercli"
setting = "enemizercli"
# set storagevar
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[setting])
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["frame"].label.configure(text=label)
# set get from web label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget + ".online")
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["online"].label.configure(text=label)
# Get baserom path # Get baserom path
gui.pages["randomizer"].pages["generation"].romVar.set(args["rom"]) mainpage = "randomizer"
subpage = "generation"
widget = "rom"
setting = "rom"
# set storagevar
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[setting])
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["frame"].label.configure(text=label)
# set button label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget + ".button")
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["button"].configure(text=label)
# Get Multiworld Worlds count # Get Multiworld Worlds count
if args["multi"]: mainpage = "randomizer"
gui.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.set(str(args["multi"])) subpage = "multiworld"
widget = "worlds"
setting = "multi"
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
if args[setting]:
# set storagevar
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(str(args[setting]))
# Set Multiworld Names
mainpage = "randomizer"
subpage = "multiworld"
widget = "names"
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["frame"].label.configure(text=label)
# Get Seed ID # Get Seed ID
if args["seed"]: mainpage = "bottom"
gui.frames["bottom"].seedVar.set(str(args["seed"])) subpage = "content"
widget = "seed"
setting = "seed"
if args[setting]:
gui.pages[mainpage].widgets[widget].storageVar.set(args[setting])
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["frame"].label.configure(text=label)
# Get number of generations to run # Get number of generations to run
if args["count"]: mainpage = "bottom"
gui.frames["bottom"].widgets["generationcount"].storageVar.set(str(args["count"])) subpage = "content"
widget = "generationcount"
setting = "count"
if args[setting]:
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(str(args[setting]))
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].label.configure(text=label)
# Set Generate button
mainpage = "bottom"
subpage = "content"
widget = "go"
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["button"].configure(text=label)
# Set Output Directory button
mainpage = "bottom"
subpage = "content"
widget = "outputdir"
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["button"].configure(text=label)
# Get output path # Get output path
gui.outputPath.set(args["outputpath"]) gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args["outputpath"])
# Set Documentation button
mainpage = "bottom"
subpage = "content"
widget = "docs"
if widget in gui.pages[mainpage].pages[subpage].widgets:
if "button" in gui.pages[mainpage].pages[subpage].widgets[widget].pieces:
# set textbox/frame label
label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget)
gui.pages[mainpage].pages[subpage].widgets[widget].pieces["button"].configure(text=label)
# Figure out Sprite Selection # Figure out Sprite Selection
def sprite_setter(spriteObject): def sprite_setter(spriteObject):

View File

@@ -3,6 +3,7 @@ import source.gui.widgets as widgets
import json import json
import os import os
import webbrowser import webbrowser
from source.classes.Empty import Empty
def enemizer_page(parent,settings): def enemizer_page(parent,settings):
def open_enemizer_download(_evt): def open_enemizer_download(_evt):
@@ -46,22 +47,43 @@ def enemizer_page(parent,settings):
## Enemizer CLI Path ## Enemizer CLI Path
# This one's more-complicated, build it and stuff it # This one's more-complicated, build it and stuff it
enemizerPathFrame = Frame(self.frames["bottomEnemizerFrame"]) # widget ID
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") widget = "enemizercli"
enemizerCLIlabel.pack(side=LEFT)
enemizerURL = Label(enemizerPathFrame, text="(get online)", fg="blue", cursor="hand2") # Empty object
enemizerURL.pack(side=LEFT) self.widgets[widget] = Empty()
enemizerURL.bind("<Button-1>", open_enemizer_download) # pieces
self.enemizerCLIpathVar = StringVar(value=settings["enemizercli"]) self.widgets[widget].pieces = {}
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=self.enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT, fill=X, expand=True) # frame
self.widgets[widget].pieces["frame"] = Frame(self.frames["bottomEnemizerFrame"])
# frame: label
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text="EnemizerCLI path: ")
self.widgets[widget].pieces["frame"].label.pack(side=LEFT)
# get app online
self.widgets[widget].pieces["online"] = Empty()
# get app online: label
self.widgets[widget].pieces["online"].label = Label(self.widgets[widget].pieces["frame"], text="(get online)", fg="blue", cursor="hand2")
self.widgets[widget].pieces["online"].label.pack(side=LEFT)
# get app online: open browser
self.widgets[widget].pieces["online"].label.bind("<Button-1>", open_enemizer_download)
# storage var
self.widgets[widget].storageVar = StringVar(value=settings["enemizercli"])
# textbox
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], textvariable=self.widgets[widget].storageVar)
self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True)
def EnemizerSelectPath(): def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join(".")) path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join("."))
if path: if path:
self.enemizerCLIpathVar.set(path) self.widgets[widget].storageVar.set(path)
settings["enemizercli"] = path settings["enemizercli"] = path
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath) # dialog button
enemizerCLIbrowseButton.pack(side=LEFT) self.widgets[widget].pieces["opendialog"] = Button(self.widgets[widget].pieces["frame"], text='...', command=EnemizerSelectPath)
enemizerPathFrame.pack(fill=X) self.widgets[widget].pieces["opendialog"].pack(side=LEFT)
# frame: pack
self.widgets[widget].pieces["frame"].pack(fill=X)
return self,settings return self,settings

View File

@@ -2,6 +2,7 @@ from tkinter import ttk, filedialog, StringVar, Button, Entry, Frame, Label, E,
import source.gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os import os
from source.classes.Empty import Empty
def generation_page(parent,settings): def generation_page(parent,settings):
# Generation Setup # Generation Setup
@@ -28,20 +29,38 @@ def generation_page(parent,settings):
self.frames["baserom"].pack(anchor=W, fill=X) self.frames["baserom"].pack(anchor=W, fill=X)
## Locate base ROM ## Locate base ROM
# This one's more-complicated, build it and stuff it # This one's more-complicated, build it and stuff it
baseRomFrame = Frame(self.frames["baserom"]) # widget ID
baseRomLabel = Label(baseRomFrame, text='Base Rom: ') widget = "rom"
self.romVar = StringVar()
romEntry = Entry(baseRomFrame, textvariable=self.romVar)
self.romVar.set(settings["rom"])
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# frame
self.widgets[widget].pieces["frame"] = Frame(self.frames["baserom"])
# frame: label
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text='Base Rom: ')
# storage var
self.widgets[widget].storageVar = StringVar()
# textbox
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], textvariable=self.widgets[widget].storageVar)
self.widgets[widget].storageVar.set(settings["rom"])
# FIXME: Translate these
def RomSelect(): def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")], initialdir=os.path.join(".")) rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")], initialdir=os.path.join("."))
self.romVar.set(rom) self.widgets[widget].storageVar.set(rom)
romSelectButton = Button(baseRomFrame, text='Select Rom', command=RomSelect) # dialog button
self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"], text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT) # frame label: pack
romEntry.pack(side=LEFT, fill=X, expand=True) self.widgets[widget].pieces["frame"].label.pack(side=LEFT)
romSelectButton.pack(side=LEFT) # textbox: pack
baseRomFrame.pack(fill=X) self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True)
# button: pack
self.widgets[widget].pieces["button"].pack(side=LEFT)
# frame: pack
self.widgets[widget].pieces["frame"].pack(fill=X)
return self,settings return self,settings

View File

@@ -2,6 +2,7 @@ from tkinter import ttk, StringVar, Entry, Frame, Label, N, E, W, X, LEFT
import source.gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os import os
from source.classes.Empty import Empty
def multiworld_page(parent,settings): def multiworld_page(parent,settings):
# Multiworld # Multiworld
@@ -26,16 +27,33 @@ def multiworld_page(parent,settings):
## List of Player Names ## List of Player Names
# This one's more-complicated, build it and stuff it # This one's more-complicated, build it and stuff it
key = "names" # widget ID
self.widgets[key] = Frame(self.frames["widgets"]) widget = "names"
self.widgets[key].label = Label(self.widgets[key], text='Player names')
self.widgets[key].storageVar = StringVar(value=settings["names"]) # Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# frame
self.widgets[widget].pieces["frame"] = Frame(self.frames["widgets"])
# frame: label
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text='Player names')
# storage var
self.widgets[widget].storageVar = StringVar(value=settings["names"])
# FIXME: Got some strange behavior here; both Entry-like objects react to mousewheel on Spinbox
def saveMultiNames(caller,_,mode): def saveMultiNames(caller,_,mode):
settings["names"] = self.widgets[key].storageVar.get() settings["names"] = self.widgets["names"].storageVar.get()
self.widgets[key].storageVar.trace_add("write",saveMultiNames) self.widgets[widget].storageVar.trace_add("write",saveMultiNames)
self.widgets[key].textbox = Entry(self.widgets[key], textvariable=self.widgets[key].storageVar) # textbox
self.widgets[key].label.pack(side=LEFT) self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], textvariable=self.widgets[widget].storageVar)
self.widgets[key].textbox.pack(side=LEFT, fill=X, expand=True)
self.widgets[key].pack(anchor=N, fill=X, expand=True) # frame label: pack
self.widgets[widget].pieces["frame"].label.pack(side=LEFT, anchor=N)
# textbox: pack
self.widgets[widget].pieces["textbox"].pack(side=LEFT, anchor=N, fill=X, expand=True)
# frame: pack
self.widgets[widget].pieces["frame"].pack(side=LEFT, anchor=N, fill=X, expand=True)
return self,settings return self,settings

View File

@@ -1,8 +1,5 @@
from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, RIGHT, X from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, RIGHT, X
from source.classes.Empty import Empty
# Need a dummy class
class Empty():
pass
# Override Spinbox to include mousewheel support for changing value # Override Spinbox to include mousewheel support for changing value
class mySpinbox(Spinbox): class mySpinbox(Spinbox):
@@ -31,27 +28,81 @@ def make_checkbox(self, parent, label, storageVar, manager, managerAttrs):
# Make an OptionMenu with a label and pretty option labels # Make an OptionMenu with a label and pretty option labels
def make_selectbox(self, parent, label, options, storageVar, manager, managerAttrs): 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 = Frame(parent, name="selectframe-" + label.lower())
self.storageVar = storageVar
self.storageVar.trace_add("write",change_selected) labels = options
if isinstance(options,dict):
labels = options.keys()
self.labelVar = StringVar() self.labelVar = StringVar()
self.storageVar = storageVar
self.selectbox = OptionMenu(self, self.labelVar, *labels)
self.selectbox.options = {}
if isinstance(options,dict):
self.selectbox.options["labels"] = list(options.keys())
self.selectbox.options["values"] = list(options.values())
else:
self.selectbox.options["labels"] = ["" for i in range(0,len(options))]
self.selectbox.options["values"] = options
def change_thing(thing, *args):
labels = self.selectbox.options["labels"]
values = self.selectbox.options["values"]
check = ""
lbl = ""
val = ""
idx = 0
if thing == "storage":
check = self.labelVar.get()
elif thing == "label":
check = self.storageVar.get()
if check in labels:
idx = labels.index(check)
if check in values:
idx = values.index(check)
lbl = labels[idx]
val = values[idx]
if thing == "storage":
self.storageVar.set(val)
elif thing == "label":
self.labelVar.set(lbl)
self.selectbox["menu"].entryconfigure(idx,label=lbl)
self.selectbox.configure(state="active")
def change_storage(*args):
change_thing("storage", *args)
def change_selected(*args):
change_thing("label", *args)
self.storageVar.trace_add("write",change_selected)
self.labelVar.trace_add("write",change_storage) self.labelVar.trace_add("write",change_storage)
self.label = Label(self, text=label) self.label = Label(self, text=label)
if managerAttrs is not None and "label" in managerAttrs: if managerAttrs is not None and "label" in managerAttrs:
self.label.pack(managerAttrs["label"]) self.label.pack(managerAttrs["label"])
else: else:
self.label.pack() self.label.pack()
self.selectbox = OptionMenu(self, self.labelVar, *options.keys())
self.selectbox.config(width=20) self.selectbox.config(width=20)
self.labelVar.set(managerAttrs["default"] if "default" in managerAttrs else list(options.keys())[0]) idx = 0
default = self.selectbox.options["values"][idx]
if "default" in managerAttrs:
default = managerAttrs["default"]
labels = self.selectbox.options["labels"]
values = self.selectbox.options["values"]
if default in values:
idx = values.index(default)
self.labelVar.set(labels[idx])
self.storageVar.set(values[idx])
self.selectbox["menu"].entryconfigure(idx,label=labels[idx])
if managerAttrs is not None and "selectbox" in managerAttrs: if managerAttrs is not None and "selectbox" in managerAttrs:
self.selectbox.pack(managerAttrs["selectbox"]) self.selectbox.pack(managerAttrs["selectbox"])
else: else:
@@ -144,6 +195,7 @@ def make_widget_from_dict(self, defn, parent):
managerAttrs = defn["managerAttrs"] if "managerAttrs" in defn else None managerAttrs = defn["managerAttrs"] if "managerAttrs" in defn else None
options = defn["options"] if "options" in defn else None options = defn["options"] if "options" in defn else None
widget = make_widget(self, type, parent, label, None, manager, managerAttrs, options) widget = make_widget(self, type, parent, label, None, manager, managerAttrs, options)
widget.type = type
return widget return widget
# Make a set of generic widgets from a dict # Make a set of generic widgets from a dict