diff --git a/BaseClasses.py b/BaseClasses.py index e90b38da..bda6832d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -4,6 +4,7 @@ import logging import json from collections import OrderedDict, deque, defaultdict +from source.classes.BabelFish import BabelFish from EntranceShuffle import door_addresses from _vendor.collections_extended import bag from Utils import int16_as_bytes @@ -71,6 +72,7 @@ class World(object): self.key_logic = {} self.pool_adjustment = {} self.key_layout = defaultdict(dict) + self.fish = BabelFish() for player in range(1, players + 1): # 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'.join( ['%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 '=>', - entry['exit'], + self.world.fish.translate("meta","doors",entry['exit']), '({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for entry in self.doors.values()])) 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'.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: + # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly 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') for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion}') + outfile.write(f'\n{dungeon}: {medallion} Medallion') if self.startinventory: outfile.write('\n\nStarting Inventory:\n\n') 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 = [] for location, path in sorted(self.paths.items()): path_lines = [] for region, exit in path: 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: - path_lines.append(region) - path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) + path_lines.append(self.world.fish.translate("meta","rooms",region)) + path_listings.append("{}\n {}".format(self.world.fish.translate("meta","locations",location), "\n => ".join(path_lines))) outfile.write('\n'.join(path_listings)) diff --git a/CLI.py b/CLI.py index ada43572..46d375ce 100644 --- a/CLI.py +++ b/CLI.py @@ -8,9 +8,8 @@ import textwrap import shlex import sys -from Main import main - import source.classes.constants as CONST +from source.classes.BabelFish import BabelFish class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): @@ -25,271 +24,56 @@ def parse_arguments(argv, no_defaults=False): # 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 parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) multiargs, _ = parser.parse_known_args(argv) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) - parser.add_argument('--create_spoiler', default=defval(settings["create_spoiler"] != 0), help='Output a Spoiler File', action='store_true') - parser.add_argument('--logic', default=defval(settings["logic"]), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'], - help='''\ - Select Enforcement of Item Requirements. (default: %(default)s) - No Glitches: - Minor Glitches: May require Fake Flippers, Bunny Revival - and Dark Room Navigation. - No Logic: Distribute items without regard for - item requirements. - ''') - parser.add_argument('--mode', default=defval(settings["mode"]), const='open', nargs='?', choices=['standard', 'open', 'inverted'], - help='''\ - Select game mode. (default: %(default)s) - Open: World starts with Zelda rescued. - Standard: Fixes Hyrule Castle Secret Entrance and Front Door - but may lead to weird rain state issues if you exit - through the Hyrule Castle side exits before rescuing - Zelda in a full shuffle. - Inverted: Starting locations are Dark Sanctuary in West Dark - World or at Link's House, which is shuffled freely. - Requires the moon pearl to be Link in the Light World - instead of a bunny. - ''') - parser.add_argument('--swords', default=defval(settings["swords"]), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'], - help='''\ - Select sword placement. (default: %(default)s) - Random: All swords placed randomly. - Assured: Start game with a sword already. - Swordless: No swords. Curtains in Skull Woods and Agahnim\'s - Tower are removed, Agahnim\'s Tower barrier can be - destroyed with hammer. Misery Mire and Turtle Rock - can be opened without a sword. Hammer damages Ganon. - Ether and Bombos Tablet can be activated with Hammer - (and Book). Bombos pads have been added in Ice - Palace, to allow for an alternative to firerod. - Vanilla: Swords are in vanilla locations. - ''') - parser.add_argument('--goal', default=defval(settings["goal"]), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'], - help='''\ - Select completion goal. (default: %(default)s) - Ganon: Collect all crystals, beat Agahnim 2 then - defeat Ganon. - Crystals: Collect all crystals then defeat Ganon. - Pedestal: Places the Triforce at the Master Sword Pedestal. - All Dungeons: Collect all crystals, pendants, beat both - Agahnim fights and then defeat Ganon. - Triforce Hunt: Places 30 Triforce Pieces in the world, collect - 20 of them to beat the game. - ''') - parser.add_argument('--difficulty', default=defval(settings["difficulty"]), const='normal', nargs='?', choices=['normal', 'hard', 'expert'], - help='''\ - Select game difficulty. Affects available itempool. (default: %(default)s) - Normal: Normal difficulty. - Hard: A harder setting with less equipment and reduced health. - Expert: A harder yet setting with minimum equipment and health. - ''') - parser.add_argument('--item_functionality', default=defval(settings["item_functionality"]), const='normal', nargs='?', choices=['normal', 'hard', 'expert'], - help='''\ - Select limits on item functionality to increase difficulty. (default: %(default)s) - Normal: Normal functionality. - Hard: Reduced functionality. - Expert: Greatly reduced functionality. - ''') - parser.add_argument('--timer', default=defval(settings["timer"]), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'], - help='''\ - Select game timer setting. Affects available itempool. (default: %(default)s) - None: No timer. - Display: Displays a timer but does not affect - the itempool. - Timed: Starts with clock at zero. Green Clocks - subtract 4 minutes (Total: 20), Blue Clocks - subtract 2 minutes (Total: 10), Red Clocks add - 2 minutes (Total: 10). Winner is player with - lowest time at the end. - Timed OHKO: Starts clock at 10 minutes. Green Clocks add - 5 minutes (Total: 25). As long as clock is at 0, - Link will die in one hit. - OHKO: Like Timed OHKO, but no clock items are present - and the clock is permenantly at zero. - Timed Countdown: Starts with clock at 40 minutes. Same clocks as - Timed mode. If time runs out, you lose (but can - still keep playing). - ''') - parser.add_argument('--progressive', default=defval(settings["progressive"]), const='normal', nargs='?', choices=['on', 'off', 'random'], - help='''\ - Select progressive equipment setting. Affects available itempool. (default: %(default)s) - On: Swords, Shields, Armor, and Gloves will - all be progressive equipment. Each subsequent - item of the same type the player finds will - upgrade that piece of equipment by one stage. - Off: Swords, Shields, Armor, and Gloves will not - be progressive equipment. Higher level items may - be found at any time. Downgrades are not possible. - Random: Swords, Shields, Armor, and Gloves will, per - category, be randomly progressive or not. - Link will die in one hit. - ''') - parser.add_argument('--algorithm', default=defval(settings["algorithm"]), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'], - help='''\ - Select item filling algorithm. (default: %(default)s - balanced: vt26 derivative that aims to strike a balance between - the overworld heavy vt25 and the dungeon heavy vt26 - algorithm. - vt26: Shuffle items and place them in a random location - that it is not impossible to be in. This includes - dungeon keys and items. - vt25: Shuffle items and place them in a random location - that it is not impossible to be in. - vt21: Unbiased in its selection, but has tendency to put - Ice Rod in Turtle Rock. - vt22: Drops off stale locations after 1/3 of progress - items were placed to try to circumvent vt21\'s - shortcomings. - Freshness: Keep track of stale locations (ones that cannot be - reached yet) and decrease likeliness of selecting - them the more often they were found unreachable. - Flood: Push out items starting from Link\'s House and - slightly biased to placing progression items with - less restrictions. - ''') - parser.add_argument('--shuffle', default=defval(settings["shuffle"]), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'], - help='''\ - Select Entrance Shuffling Algorithm. (default: %(default)s) - Full: Mix cave and dungeon entrances freely while limiting - multi-entrance caves to one world. - Simple: Shuffle Dungeon Entrances/Exits between each other - and keep all 4-entrance dungeons confined to one - location. All caves outside of death mountain are - shuffled in pairs and matched by original type. - Restricted: Use Dungeons shuffling from Simple but freely - connect remaining entrances. - Crossed: Mix cave and dungeon entrances freely while allowing - caves to cross between worlds. - Insanity: Decouple entrances and exits from each other and - shuffle them freely. Caves that used to be single - entrance will still exit to the same location from - which they are entered. - Vanilla: All entrances are in the same locations they were - in the base game. - Legacy shuffles preserve behavior from older versions of the - entrance randomizer including significant technical limitations. - The dungeon variants only mix up dungeons and keep the rest of - the overworld vanilla. - ''') - parser.add_argument('--door_shuffle', default=defval(settings["door_shuffle"]), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed'], - help='''\ - Select Door Shuffling Algorithm. (default: %(default)s) - Basic: Doors are mixed within a single dungeon. - (Not yet implemented) - Crossed: Doors are mixed between all dungeons. - (Not yet implemented) - Vanilla: All doors are connected the same way they were in the - base game. - ''') - parser.add_argument('--experimental', default=defval(settings["experimental"] != 0), help='Enable experimental features', action='store_true') - parser.add_argument('--dungeon_counters', default=defval(settings["dungeon_counters"]), help='Enable dungeon chest counters', const='off', nargs='?', choices=['off', 'on', 'pickup', 'default']) - parser.add_argument('--crystals_ganon', default=defval(settings["crystals_ganon"]), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'], - help='''\ - How many crystals are needed to defeat ganon. Any other - requirements for ganon for the selected goal still apply. - This setting does not apply when the all dungeons goal is - selected. (default: %(default)s) - Random: Picks a random value between 0 and 7 (inclusive). - 0-7: Number of crystals needed - ''') - parser.add_argument('--crystals_gt', default=defval(settings["crystals_gt"]), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'], - help='''\ - How many crystals are needed to open GT. For inverted mode - this applies to the castle tower door instead. (default: %(default)s) - Random: Picks a random value between 0 and 7 (inclusive). - 0-7: Number of crystals needed - ''') - parser.add_argument('--openpyramid', default=defval(settings["openpyramid"] != 0), help='''\ - Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it - ''', action='store_true') - parser.add_argument('--rom', default=defval(settings["rom"]), help='Path to an ALttP JAP(1.0) rom to use as a base.') - parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') - parser.add_argument('--seed', default=defval(int(settings["seed"]) if settings["seed"] != "" and settings["seed"] is not None else None), help='Define seed number to generate.', type=int) - parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else None), help='''\ - Use to batch generate multiple seeds with same settings. - If --seed is provided, it will be used for the first seed, then - used to derive the next seed (i.e. generating 10 seeds with - --seed given will produce the same 10 (different) roms each - time). - ''', type=int) - parser.add_argument('--fastmenu', default=defval(settings["fastmenu"]), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], - help='''\ - Select the rate at which the menu opens and closes. - (default: %(default)s) - ''') - parser.add_argument('--quickswap', default=defval(settings["quickswap"] != 0), help='Enable quick item swapping with L and R.', action='store_true') - parser.add_argument('--disablemusic', default=defval(settings["disablemusic"] != 0), help='Disables game music.', action='store_true') - parser.add_argument('--mapshuffle', default=defval(settings["mapshuffle"] != 0), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--compassshuffle', default=defval(settings["compassshuffle"] != 0), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--keyshuffle', default=defval(settings["keyshuffle"] != 0), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--bigkeyshuffle', default=defval(settings["bigkeyshuffle"] != 0), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') - parser.add_argument('--keysanity', default=defval(settings["keysanity"] != 0), help=argparse.SUPPRESS, action='store_true') - parser.add_argument('--retro', default=defval(settings["retro"] != 0), help='''\ - Keys are universal, shooting arrows costs rupees, - and a few other little things make this more like Zelda-1. - ''', action='store_true') - parser.add_argument('--startinventory', default=defval(settings["startinventory"]), help='Specifies a list of items that will be in your starting inventory (separated by commas)') - parser.add_argument('--usestartinventory', default=defval(settings["usestartinventory"] != 0), help='Not supported.') - parser.add_argument('--custom', default=defval(settings["custom"] != 0), help='Not supported.') - parser.add_argument('--customitemarray', default={}, help='Not supported.') - parser.add_argument('--accessibility', default=defval(settings["accessibility"]), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\ - Select Item/Location Accessibility. (default: %(default)s) - Items: You can reach all unique inventory items. No guarantees about - reaching all locations or all keys. - Locations: You will be able to reach every location in the game. - None: You will be able to reach enough locations to beat the game. - ''') - parser.add_argument('--hints', default=defval(settings["hints"] != 0), help='''\ - Make telepathic tiles and storytellers give helpful hints. - ''', action='store_true') + + # get args + args = [] + with open(os.path.join("resources","app","cli","args.json")) as argsFile: + args = json.load(argsFile) + for arg in args: + argdata = args[arg] + argname = "--" + arg + argatts = {} + argatts["help"] = "(default: %(default)s)" + if "action" in argdata: + argatts["action"] = argdata["action"] + if "choices" in argdata: + argatts["choices"] = argdata["choices"] + argatts["const"] = argdata["choices"][0] + argatts["default"] = argdata["choices"][0] + argatts["nargs"] = "?" + elif arg in settings: + argatts["default"] = defval(settings[arg] != 0) if "type" in argdata and argdata["type"] == "bool" else defval(settings[arg]) + arghelp = fish.translate("cli","help",arg) + if "help" in argdata and argdata["help"] == "suppress": + argatts["help"] = argparse.SUPPRESS + elif not isinstance(arghelp,str): + argatts["help"] = '\n'.join(arghelp).replace("\\'","'") + parser.add_argument(argname,**argatts) + + 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) + parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli","help","count")), type=int) + parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS) + # included for backwards compatibility - parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(settings["shuffleganon"] != 0)) - parser.add_argument('--no-shuffleganon', help='''\ - If set, the Pyramid Hole and Ganon's Tower are not - included entrance shuffle pool. - ''', action='store_false', dest='shuffleganon') - parser.add_argument('--heartbeep', default=defval(settings["heartbeep"]), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'], - help='''\ - Select the rate at which the heart beep sound is played at - low health. (default: %(default)s) - ''') - parser.add_argument('--heartcolor', default=defval(settings["heartcolor"]), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'], - help='Select the color of Link\'s heart meter. (default: %(default)s)') - parser.add_argument('--ow_palettes', default=defval(settings["ow_palettes"]), choices=['default', 'random', 'blackout']) - parser.add_argument('--uw_palettes', default=defval(settings["uw_palettes"]), choices=['default', 'random', 'blackout']) - parser.add_argument('--sprite', default=defval(settings["sprite"]), help='''\ - Path to a sprite sheet to use for Link. Needs to be in - binary format and have a length of 0x7000 (28672) bytes, - or 0x7078 (28792) bytes including palette data. - Alternatively, can be a ALttP Rom patched with a Link - sprite that will be extracted. - ''') - parser.add_argument('--suppress_rom', default=defval(settings["suppress_rom"] != 0), help='Do not create an output rom file.', action='store_true') - parser.add_argument('--gui', help='Launch the GUI', action='store_true') - parser.add_argument('--jsonout', action='store_true', help='''\ - Output .json patch to stdout instead of a patched rom. Used - for VT site integration, do not use otherwise. - ''') - parser.add_argument('--skip_playthrough', action='store_true', default=defval(settings["skip_playthrough"] != 0)) - parser.add_argument('--enemizercli', default=defval(settings["enemizercli"])) - parser.add_argument('--shufflebosses', default=defval(settings["shufflebosses"]), choices=['none', 'basic', 'normal', 'chaos']) - parser.add_argument('--shuffleenemies', default=defval(settings["shuffleenemies"]), choices=['none', 'shuffled', 'chaos']) - parser.add_argument('--enemy_health', default=defval(settings["enemy_health"]), choices=['default', 'easy', 'normal', 'hard', 'expert']) - parser.add_argument('--enemy_damage', default=defval(settings["enemy_damage"]), choices=['default', 'shuffled', 'chaos']) - parser.add_argument('--shufflepots', default=defval(settings["shufflepots"] != 0), action='store_true') parser.add_argument('--beemizer', default=defval(settings["beemizer"]), type=lambda value: min(max(int(value), 0), 4)) - parser.add_argument('--remote_items', default=defval(settings["remote_items"] != 0), action='store_true') parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) - parser.add_argument('--names', default=defval(settings["names"])) parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) - parser.add_argument('--outputpath', default=defval(settings["outputpath"])) - parser.add_argument('--race', default=defval(settings["race"] != 0), action='store_true') - parser.add_argument('--saveonexit', default=defval(settings["saveonexit"]), choices=['never', 'ask', 'always']) - parser.add_argument('--outputname') if multiargs.multi: for player in range(1, multiargs.multi + 1): @@ -324,6 +108,7 @@ def parse_arguments(argv, no_defaults=False): def get_settings(): # set default settings settings = { + "lang": "en", "retro": False, "mode": "open", "logic": "noglitches", @@ -378,88 +163,89 @@ def get_settings(): "custom": False, "rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"), - "seed": None, - "count": None, + "seed": "", + "count": 1, "startinventory": "", "beemizer": 0, "remote_items": False, "race": False, "customitemarray": { "bow": 0, - "progressivebow": 2, - "boomerang": 1, - "redmerang": 1, - "hookshot": 1, - "mushroom": 1, - "powder": 1, - "firerod": 1, - "icerod": 1, - "bombos": 1, - "ether": 1, - "quake": 1, - "lamp": 1, - "hammer": 1, - "shovel": 1, - "flute": 1, - "bugnet": 1, - "book": 1, - "bottle": 4, - "somaria": 1, - "byrna": 1, - "cape": 1, - "mirror": 1, - "boots": 1, - "powerglove": 0, - "titansmitt": 0, - "progressiveglove": 2, - "flippers": 1, - "pearl": 1, - "heartpiece": 24, - "heartcontainer": 10, - "sancheart": 1, - "sword1": 0, - "sword2": 0, - "sword3": 0, - "sword4": 0, - "progressivesword": 4, - "shield1": 0, - "shield2": 0, - "shield3": 0, - "progressiveshield": 3, - "mail2": 0, - "mail3": 0, - "progressivemail": 2, - "halfmagic": 1, - "quartermagic": 0, - "bombsplus5": 0, - "bombsplus10": 0, - "arrowsplus5": 0, - "arrowsplus10": 0, - "arrow1": 1, - "arrow10": 12, - "bomb1": 0, - "bomb3": 16, - "bomb10": 1, - "rupee1": 2, - "rupee5": 4, - "rupee20": 28, - "rupee50": 7, - "rupee100": 1, - "rupee300": 5, - "blueclock": 0, - "greenclock": 0, - "redclock": 0, - "silversupgrade": 0, - "generickeys": 0, - "triforcepieces": 0, - "triforcepiecesgoal": 0, - "triforce": 0, - "rupoor": 0, - "rupoorcost": 10 - }, + "progressivebow": 2, + "boomerang": 1, + "redmerang": 1, + "hookshot": 1, + "mushroom": 1, + "powder": 1, + "firerod": 1, + "icerod": 1, + "bombos": 1, + "ether": 1, + "quake": 1, + "lamp": 1, + "hammer": 1, + "shovel": 1, + "flute": 1, + "bugnet": 1, + "book": 1, + "bottle": 4, + "somaria": 1, + "byrna": 1, + "cape": 1, + "mirror": 1, + "boots": 1, + "powerglove": 0, + "titansmitt": 0, + "progressiveglove": 2, + "flippers": 1, + "pearl": 1, + "heartpiece": 24, + "heartcontainer": 10, + "sancheart": 1, + "sword1": 0, + "sword2": 0, + "sword3": 0, + "sword4": 0, + "progressivesword": 4, + "shield1": 0, + "shield2": 0, + "shield3": 0, + "progressiveshield": 3, + "mail2": 0, + "mail3": 0, + "progressivemail": 2, + "halfmagic": 1, + "quartermagic": 0, + "bombsplus5": 0, + "bombsplus10": 0, + "arrowsplus5": 0, + "arrowsplus10": 0, + "arrow1": 1, + "arrow10": 12, + "bomb1": 0, + "bomb3": 16, + "bomb10": 1, + "rupee1": 2, + "rupee5": 4, + "rupee20": 28, + "rupee50": 7, + "rupee100": 1, + "rupee300": 5, + "blueclock": 0, + "greenclock": 0, + "redclock": 0, + "silversupgrade": 0, + "generickeys": 0, + "triforcepieces": 0, + "triforcepiecesgoal": 0, + "triforce": 0, + "rupoor": 0, + "rupoorcost": 10 + }, "randomSprite": False, "outputpath": os.path.join("."), "saveonexit": "ask", + "outputname": "", "startinventoryarray": {} } diff --git a/DoorShuffle.py b/DoorShuffle.py index 8bd93efc..47e2384b 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -316,7 +316,7 @@ def within_dungeon(world, player): dungeon_builders[key] = simple_dungeon_builder(key, sector_list) dungeon_builders[key].entrance_list = list(entrances_map[key]) 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) paths = determine_required_paths(world, player) @@ -326,15 +326,15 @@ def within_dungeon(world, player): start = time.process_time() for builder in world.dungeon_layouts[player].values(): 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) -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(): builder = dungeon_builders.pop(name) 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) for sub_name, split_entrances in split_list.items(): sub_builder = dungeon_builders[name+' '+sub_name] @@ -366,7 +366,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ sector_queue.append(builder) last_key = builder.name 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) find_new_entrances(ds, connections, potentials, enabled_entrances, world, player) ds.name = name @@ -501,7 +501,7 @@ def shuffle_dungeon(world, player, start_region_names, dungeon_region_names): for door in get_doors(world, world.get_region(name, player), player): ugly_regions[door.name] = 0 available_doors.append(door) - + # Loop until all available doors are used while len(available_doors) > 0: # Pick a random available door to connect, prioritizing ones that aren't blocked. @@ -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] loc.forced_item = loc.item = ItemFactory(key_name, player) 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) @@ -792,7 +792,7 @@ def assign_cross_keys(dungeon_builders, world, player): dungeon.small_keys = [] else: 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): @@ -949,14 +949,14 @@ def calc_used_dungeon_items(builder): def find_valid_combination(builder, start_regions, world, player, drop_keys=True): 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 if len(builder.candidates) < builder.key_doors_num: if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) return False 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) itr = 0 start = time.process_time() @@ -976,7 +976,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True if not drop_keys: logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) 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 if builder.key_doors_num < 0: raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5dd74be4..2c009437 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1149,8 +1149,8 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None): # polarity: if not global_pole.is_valid(dungeon_map): raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!') - logger.info('-Balancing Doors') - assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) + logger.info(world.fish.translate("cli","cli","balance.doors")) + assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, world.fish) # the rest assign_the_rest(dungeon_map, neutral_sectors, global_pole) return dungeon_map @@ -1434,9 +1434,9 @@ def sum_polarity(sector_list): 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 - logger.info('--Basic Traversal') + logger.info(fish.translate("cli","cli","basic.traversal")) unconnected_builders = identify_polarity_issues(dungeon_map) while len(unconnected_builders) > 0: 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) # 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 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 -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()) random.shuffle(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(): candidates = find_neutralizing_candidates(builder, polarized_sectors) 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) -def split_dungeon_builder(builder, split_list): +def split_dungeon_builder(builder, split_list, fish): 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) global_pole = GlobalPolarity(candidate_sectors) @@ -1844,10 +1844,10 @@ def split_dungeon_builder(builder, split_list): sub_builder.all_entrances = split_entrances for r_name in split_entrances: 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('') # categorize 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 assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) # polarity: - logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') - assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) + 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, fish) # the rest assign_the_rest(dungeon_map, neutral_sectors, global_pole) return dungeon_map diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index 64a0e2d7..4a0c04e3 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -8,7 +8,9 @@ import textwrap import shlex 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 Rom import get_sprite_from_name 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] 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: from Gui import guiMain guiMain(args) @@ -51,11 +59,11 @@ def start(): logger = logging.getLogger('') for _ in range(args.count): try: - main(seed=seed, args=args) - logger.info('Finished run %s', _+1) + main(seed=seed, args=args, fish=fish) + logger.info('%s %s', fish.translate("cli","cli","finished.run"), _+1) except (FillError, Exception, RuntimeError) as err: 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) for fail in failures: 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 success rate: ' + str(success_rate[0]).rjust(3, " ") + '.' + str(success_rate[1]).ljust(6, '0') + '%') else: - main(seed=args.seed, args=args) + main(seed=args.seed, args=args, fish=fish) if __name__ == '__main__': diff --git a/Gui.py b/Gui.py index c2f871a5..5093ba5e 100755 --- a/Gui.py +++ b/Gui.py @@ -21,6 +21,9 @@ from source.gui.bottom import bottom_frame, create_guiargs from GuiUtils import set_icon from Main import __version__ as ESVersion +from source.classes.BabelFish import BabelFish +from source.classes.Empty import Empty + def guiMain(args=None): # Save settings to file @@ -143,14 +146,22 @@ def guiMain(args=None): # add randomizer notebook to main window 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) - 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 - 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) # 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.randomSprite = BooleanVar() diff --git a/Main.py b/Main.py index e71dd8d3..8a22e47b 100644 --- a/Main.py +++ b/Main.py @@ -27,7 +27,7 @@ from Utils import output_path, parse_player_names, print_wiki_doors_by_region, p __version__ = '0.0.18.4d' -def main(args, seed=None): +def main(args, seed=None, fish=None): if args.outputpath: os.makedirs(args.outputpath, exist_ok=True) output_path.cached_path = args.outputpath @@ -59,10 +59,15 @@ def main(args, seed=None): world.beemizer = args.beemizer.copy() world.experimental = args.experimental.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)} - 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) world.teams = len(parsed_names) @@ -95,7 +100,7 @@ def main(args, seed=None): create_rooms(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): if world.mode[player] != 'inverted': @@ -103,7 +108,7 @@ def main(args, seed=None): else: 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): link_doors(world, player) @@ -111,21 +116,21 @@ def main(args, seed=None): mark_light_world_regions(world, player) else: 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): 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): set_rules(world, player) - logger.info('Placing Dungeon Prizes.') + logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) fill_prizes(world) - logger.info('Placing Dungeon Items.') + logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) shuffled_locations = None 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 key_layout in world.key_layout[player].values(): 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': 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) if world.players > 1: - logger.info('Balancing multiworld progression.') + logger.info(world.fish.translate("cli","cli","balance.multiworld")) balance_multiworld_progression(world) # if we only check for beatable, we can do this sanity check first before creating the rom 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) @@ -191,8 +204,8 @@ def main(args, seed=None): if not args.jsonout: rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000) else: - logging.warning("EnemizerCLI not found at:" + args.enemizercli) - logging.warning("No Enemizer options will be applied until this is resolved.") + logging.warning(world.fish.translate("cli","cli","enemizer.not.found") + ': ' + args.enemizercli) + logging.warning(world.fish.translate("cli","cli","enemizer.nothing.applied")) if args.race: patch_race_rom(rom) @@ -246,7 +259,7 @@ def main(args, seed=None): world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) if not args.skip_playthrough: - logger.info('Calculating playthrough.') + logger.info(world.fish.translate("cli","cli","calc.playthrough")) create_playthrough(world) if args.jsonout: @@ -254,8 +267,9 @@ def main(args, seed=None): elif args.create_spoiler and not args.skip_playthrough: world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) - logger.info('Done. Enjoy.') - logger.info('Total Time: %s', time.perf_counter() - start) + logger.info(world.fish.translate("cli","cli","done")) + 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_region(dungeon_regions,world,1) @@ -402,7 +416,7 @@ def create_playthrough(world): collection_spheres = [] state = CollectionState(world) 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: state.sweep_for_events(key_only=True) state.sweep_for_crystal_access() @@ -421,11 +435,11 @@ def create_playthrough(world): 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: - 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]): - raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.') + raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.progression")) else: old_world.spoiler.unreachables = sphere_candidates.copy() break @@ -477,9 +491,9 @@ def create_playthrough(world): 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: - 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 old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json new file mode 100644 index 00000000..f95f3a1d --- /dev/null +++ b/resources/app/cli/args.json @@ -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": {} +} diff --git a/resources/app/cli/lang/de.json b/resources/app/cli/lang/de.json new file mode 100644 index 00000000..07936374 --- /dev/null +++ b/resources/app/cli/lang/de.json @@ -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." + } +} diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json new file mode 100644 index 00000000..53ef9599 --- /dev/null +++ b/resources/app/cli/lang/en.json @@ -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)" + ] + } +} diff --git a/resources/app/cli/lang/es.json b/resources/app/cli/lang/es.json new file mode 100644 index 00000000..9dcdc2c0 --- /dev/null +++ b/resources/app/cli/lang/es.json @@ -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." + } +} diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json new file mode 100644 index 00000000..2fc10054 --- /dev/null +++ b/resources/app/gui/lang/en.json @@ -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" + } +} diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 295f3098..be8973d8 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -11,13 +11,13 @@ "selectbox": { "side": "right" }, - "default": "Basic" + "default": "basic" }, - "options": { - "Vanilla": "vanilla", - "Basic": "basic", - "Crossed": "crossed" - } + "options": [ + "vanilla", + "basic", + "crossed" + ] }, "experimental": { "type": "checkbox", @@ -37,13 +37,13 @@ "selectbox": { "side": "right" }, - "default": "Auto" + "default": "default" }, - "options": { - "Auto": "default", - "Off": "off", - "On": "on", - "On Compass Pickup": "pickup" - } + "options": [ + "default", + "off", + "on", + "pickup" + ] } } diff --git a/resources/app/gui/randomize/enemizer/widgets.json b/resources/app/gui/randomize/enemizer/widgets.json index 4095ab5b..af18a221 100644 --- a/resources/app/gui/randomize/enemizer/widgets.json +++ b/resources/app/gui/randomize/enemizer/widgets.json @@ -21,11 +21,11 @@ "side": "right" } }, - "options": { - "Vanilla": "none", - "Shuffled": "shuffled", - "Chaos": "chaos" - } + "options": [ + "none", + "shuffled", + "chaos" + ] }, "bossshuffle": { "type": "selectbox", @@ -40,12 +40,12 @@ "side": "right" } }, - "options": { - "Vanilla": "none", - "Basic": "basic", - "Shuffled": "shuffled", - "Chaos": "chaos" - } + "options": [ + "none", + "basic", + "shuffled", + "chaos" + ] } }, "rightEnemizerFrame": { @@ -62,11 +62,11 @@ "side": "right" } }, - "options": { - "Vanilla": "default", - "Shuffled": "shuffled", - "Chaos": "chaos" - } + "options": [ + "default", + "shuffled", + "chaos" + ] }, "enemyhealth": { "type": "selectbox", @@ -81,13 +81,13 @@ "side": "right" } }, - "options": { - "Vanilla": "default", - "Easy": "easy", - "Normal": "normal", - "Hard": "hard", - "Expert": "expert" - } + "options": [ + "default", + "easy", + "normal", + "hard", + "expert" + ] } } } \ No newline at end of file diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index 1a3ae127..f1c8aad6 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -21,20 +21,20 @@ "label": { "side": "left" }, "selectbox": { "side": "right" } }, - "options": { - "Vanilla": "vanilla", - "Simple": "simple", - "Restricted": "restricted", - "Full": "full", - "Crossed": "crossed", - "Insanity": "insanity", - "Restricted (Legacy)": "restricted_legacy", - "Full (Legacy)": "full_legacy", - "Madness (Legacy)": "madness_legacy", - "Insanity (Legacy)": "insanity_legacy", - "Dungeons + Full": "dungeonsfull", - "Dungeons + Simple": "dungeonssimple" - } + "options": [ + "vanilla", + "simple", + "restricted", + "full", + "crossed", + "insanity", + "restricted_legacy", + "full_legacy", + "madness_legacy", + "insanity_legacy", + "dungeonsfull", + "dungeonssimple" + ] } } } diff --git a/resources/app/gui/randomize/gameoptions/widgets.json b/resources/app/gui/randomize/gameoptions/widgets.json index 0a0a11ce..f84ba3ee 100644 --- a/resources/app/gui/randomize/gameoptions/widgets.json +++ b/resources/app/gui/randomize/gameoptions/widgets.json @@ -18,7 +18,7 @@ "label": { "text": "L/R Quickswapping" } - } + } }, "leftRomOptionsFrame": { "heartcolor": { @@ -34,13 +34,13 @@ "side": "right" } }, - "options": { - "Red": "red", - "Blue": "blue", - "Green": "green", - "Yellow": "yellow", - "Random": "random" - } + "options": [ + "red", + "blue", + "green", + "yellow", + "random" + ] }, "heartbeep": { "type": "selectbox", @@ -54,15 +54,15 @@ "selectbox": { "side": "right" }, - "default": "Normal" + "default": "normal" }, - "options": { - "Double": "double", - "Normal": "normal", - "Half": "half", - "Quarter": "quarter", - "Off": "off" - } + "options": [ + "double", + "normal", + "half", + "quarter", + "off" + ] } }, "rightRomOptionsFrame": { @@ -78,16 +78,16 @@ "selectbox": { "side": "right" }, - "default": "Normal" + "default": "normal" }, - "options": { - "Instant": "instant", - "Quadruple": "quadruple", - "Triple": "triple", - "Double": "double", - "Normal": "normal", - "Half": "half" - } + "options": [ + "instant", + "quadruple", + "triple", + "double", + "normal", + "half" + ] }, "owpalettes": { "type": "selectbox", @@ -102,11 +102,11 @@ "side": "right" } }, - "options": { - "Default": "default", - "Random": "random", - "Blackout": "blackout" - } + "options": [ + "default", + "random", + "blackout" + ] }, "uwpalettes": { "type": "selectbox", @@ -121,11 +121,11 @@ "side": "right" } }, - "options": { - "Default": "default", - "Random": "random", - "Blackout": "blackout" - } + "options": [ + "default", + "random", + "blackout" + ] } } } diff --git a/resources/app/gui/randomize/generation/checkboxes.json b/resources/app/gui/randomize/generation/checkboxes.json index b7228c78..43693b72 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -36,10 +36,10 @@ "side": "right" } }, - "options": { - "Ask Me": "ask", - "Always": "always", - "Never": "never" - } + "options": [ + "ask", + "always", + "never" + ] } } diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index f63cffe7..96dcaa99 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -20,14 +20,14 @@ "selectbox": { "side": "right" }, - "default": "Open" + "default": "open" }, - "options": { - "Standard": "standard", - "Open": "open", - "Inverted": "inverted", - "Retro": "retro" - } + "options": [ + "standard", + "open", + "inverted", + "retro" + ] }, "logiclevel": { "type": "selectbox", @@ -42,11 +42,11 @@ "side": "right" } }, - "options": { - "No Glitches": "noglitches", - "Minor Glitches": "minorglitches", - "No Logic": "nologic" - } + "options": [ + "noglitches", + "minorglitches", + "nologic" + ] }, "goal": { "type": "selectbox", @@ -61,13 +61,13 @@ "side": "right" } }, - "options": { - "Defeat Ganon": "ganon", - "Master Sword Pedestal": "pedestal", - "All Dungeons": "dungeons", - "Triforce Hunt": "triforcehunt", - "Crystals": "crystals" - } + "options": [ + "ganon", + "pedestal", + "dungeons", + "triforcehunt", + "crystals" + ] }, "crystals_gt": { "type": "selectbox", @@ -82,17 +82,17 @@ "side": "right" } }, - "options": { - "0": "0", - "1": "1", - "2": "2", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "Random": "random" - } + "options": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "random" + ] }, "crystals_ganon": { "type": "selectbox", @@ -107,17 +107,17 @@ "side": "right" } }, - "options": { - "0": "0", - "1": "1", - "2": "2", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "Random": "random" - } + "options": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "random" + ] }, "weapons": { "type": "selectbox", @@ -132,12 +132,12 @@ "side": "right" } }, - "options": { - "Randomized": "random", - "Assured": "assured", - "Swordless": "swordless", - "Vanilla": "vanilla" - } + "options": [ + "random", + "assured", + "swordless", + "vanilla" + ] } }, "rightItemFrame": { @@ -154,11 +154,11 @@ "side": "right" } }, - "options": { - "Normal": "normal", - "Hard": "hard", - "Expert": "expert" - } + "options": [ + "normal", + "hard", + "expert" + ] }, "itemfunction": { "type": "selectbox", @@ -173,11 +173,11 @@ "side": "right" } }, - "options": { - "Normal": "normal", - "Hard": "hard", - "Expert": "expert" - } + "options": [ + "normal", + "hard", + "expert" + ] }, "timer": { "type": "selectbox", @@ -192,14 +192,14 @@ "side": "right" } }, - "options": { - "No Timer": "none", - "Stopwatch": "display", - "Timed": "timed", - "Timed OHKO": "timed-ohko", - "OHKO": "ohko", - "Timed Countdown": "timed-countdown" - } + "options": [ + "none", + "display", + "timed", + "timed-ohko", + "ohko", + "timed-countdown" + ] }, "progressives": { "type": "selectbox", @@ -214,11 +214,11 @@ "side": "right" } }, - "options": { - "On": "on", - "Off": "off", - "Random": "random" - } + "options": [ + "on", + "off", + "random" + ] }, "accessibility": { "type": "selectbox", @@ -233,11 +233,11 @@ "side": "right" } }, - "options": { - "100% Inventory": "items", - "100% Locations": "locations", - "Beatable": "none" - } + "options": [ + "items", + "locations", + "none" + ] }, "sortingalgo": { "type": "selectbox", @@ -251,17 +251,17 @@ "selectbox": { "side": "right" }, - "default": "Balanced" + "default": "balanced" }, - "options": { - "Freshness": "freshness", - "Flood": "flood", - "VT8.21": "vt21", - "VT8.22": "vt22", - "VT8.25": "vt25", - "VT8.26": "vt26", - "Balanced": "balanced" - } + "options": [ + "freshness", + "flood", + "vt21", + "vt22", + "vt25", + "vt26", + "balanced" + ] } } } diff --git a/resources/app/meta/lang/en.json b/resources/app/meta/lang/en.json new file mode 100644 index 00000000..0db3279e --- /dev/null +++ b/resources/app/meta/lang/en.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/source/classes/BabelFish.py b/source/classes/BabelFish.py new file mode 100644 index 00000000..3be79c09 --- /dev/null +++ b/source/classes/BabelFish.py @@ -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 diff --git a/source/classes/Empty.py b/source/classes/Empty.py new file mode 100644 index 00000000..a22a92d1 --- /dev/null +++ b/source/classes/Empty.py @@ -0,0 +1,3 @@ +# Need a dummy class +class Empty(): + pass diff --git a/source/classes/constants.py b/source/classes/constants.py index 8277cd3c..9e52ebc6 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -110,5 +110,11 @@ SETTINGSTOPROCESS = { "usecustompool": "custom", "saveonexit": "saveonexit" } + }, + "bottom": { + "content": { + "seed": "seed", + "generationcount": "count" + } } } diff --git a/source/gui/bottom.py b/source/gui/bottom.py index aeb26c94..b55090ea 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -8,6 +8,7 @@ from Main import main from Utils import local_path, output_path, open_file import source.classes.constants as CONST import source.gui.widgets as widgets +from source.classes.Empty import Empty def bottom_frame(self, parent, args=None): @@ -17,19 +18,34 @@ def bottom_frame(self, parent, args=None): # Bottom Frame options self.widgets = {} - seedCountFrame = Frame(self) - seedCountFrame.pack() - ## Seed # - seedLabel = Label(self, text='Seed #') + # Seed input + # widget ID + widget = "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"] - 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): - savedSeed = self.seedVar.get() + savedSeed = self.widgets["seed"].storageVar.get() parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None - self.seedVar.trace_add("write",saveSeed) - seedEntry = Entry(self, width=15, textvariable=self.seedVar) - seedLabel.pack(side=LEFT) - seedEntry.pack(side=LEFT) + self.widgets[widget].storageVar.trace_add("write",saveSeed) + # frame: pack + self.widgets[widget].pieces["frame"].pack(side=LEFT) ## Number of Generation attempts key = "generationcount" @@ -56,10 +72,10 @@ def bottom_frame(self, parent, args=None): if guiargs.count is not None: seed = guiargs.seed for _ in range(guiargs.count): - main(seed=seed, args=guiargs) + main(seed=seed, args=guiargs, fish=parent.fish) seed = random.randint(0, 999999999) else: - main(seed=guiargs.seed, args=guiargs) + main(seed=guiargs.seed, args=guiargs, fish=parent.fish) except Exception as e: logging.exception(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") ## Generate Button - generateButton = Button(self, text='Generate Patched Rom', command=generateRom) - generateButton.pack(side=LEFT) + # widget ID + 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(): if args and args.outputpath: @@ -76,15 +103,42 @@ def bottom_frame(self, parent, args=None): else: open_file(output_path(parent.settings["outputpath"])) - openOutputButton = Button(self, text='Open Output Directory', command=open_output) - openOutputButton.pack(side=RIGHT) + ## Output Button + # 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 + # 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')): def open_readme(): open_file(local_path('README.html')) - openReadmeButton = Button(self, text='Open Documentation', command=open_readme) - openReadmeButton.pack(side=RIGHT) + self.widgets[widget].pieces["button"] = Button(self, text='Open Documentation', command=open_readme) + # button: pack + self.widgets[widget].pieces["button"].pack(side=RIGHT) return self @@ -107,22 +161,26 @@ def create_guiargs(parent): setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get()) # 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 guiargs.multi = int(parent.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.get()) # 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 guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get()) # 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 - 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 adjustargs = { diff --git a/source/gui/loadcliargs.py b/source/gui/loadcliargs.py index b807b599..a528fa50 100644 --- a/source/gui/loadcliargs.py +++ b/source/gui/loadcliargs.py @@ -2,10 +2,13 @@ from source.classes.SpriteSelector import SpriteSelector as spriteSelector from source.gui.randomize.gameoptions import set_sprite from Rom import Sprite, get_sprite_from_name import source.classes.constants as CONST +from source.classes.BabelFish import BabelFish +from source.classes.Empty import Empty # Load args/settings for most tabs def loadcliargs(gui, args, settings=None): if args is not None: + fish = BabelFish() # for k, v in vars(args).items(): # if type(v) is dict: # setattr(args, k, v[1]) # only get values for player 1 for now @@ -21,39 +24,146 @@ def loadcliargs(gui, args, settings=None): for subpage in options[mainpage]: # Cycle through each widget for widget in options[mainpage][subpage]: - # Get the value and set it - arg = options[mainpage][subpage][widget] - 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 - 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]) + if widget in gui.pages[mainpage].pages[subpage].widgets: + thisType = "" + # Get the value and set it + arg = options[mainpage][subpage][widget] + if args[arg] == None: + args[arg] = "" + label = fish.translate("gui","gui",mainpage + '.' + subpage + '.' + widget) + if hasattr(gui.pages[mainpage].pages[subpage].widgets[widget],"type"): + thisType = gui.pages[mainpage].pages[subpage].widgets[widget].type + if thisType == "checkbox": + gui.pages[mainpage].pages[subpage].widgets[widget].checkbox.configure(text=label) + 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 - 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 - 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 - if args["multi"]: - gui.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.set(str(args["multi"])) + mainpage = "randomizer" + 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 - if args["seed"]: - gui.frames["bottom"].seedVar.set(str(args["seed"])) + mainpage = "bottom" + 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 - if args["count"]: - gui.frames["bottom"].widgets["generationcount"].storageVar.set(str(args["count"])) + mainpage = "bottom" + 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 - 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 def sprite_setter(spriteObject): diff --git a/source/gui/randomize/enemizer.py b/source/gui/randomize/enemizer.py index 8c92bd48..f6ba1846 100644 --- a/source/gui/randomize/enemizer.py +++ b/source/gui/randomize/enemizer.py @@ -3,6 +3,7 @@ import source.gui.widgets as widgets import json import os import webbrowser +from source.classes.Empty import Empty def enemizer_page(parent,settings): def open_enemizer_download(_evt): @@ -46,22 +47,43 @@ def enemizer_page(parent,settings): ## Enemizer CLI Path # This one's more-complicated, build it and stuff it - enemizerPathFrame = Frame(self.frames["bottomEnemizerFrame"]) - enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") - enemizerCLIlabel.pack(side=LEFT) - enemizerURL = Label(enemizerPathFrame, text="(get online)", fg="blue", cursor="hand2") - enemizerURL.pack(side=LEFT) - enemizerURL.bind("", open_enemizer_download) - self.enemizerCLIpathVar = StringVar(value=settings["enemizercli"]) - enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=self.enemizerCLIpathVar) - enemizerCLIpathEntry.pack(side=LEFT, fill=X, expand=True) + # widget ID + widget = "enemizercli" + + # Empty object + self.widgets[widget] = Empty() + # pieces + self.widgets[widget].pieces = {} + + # 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("", 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(): path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join(".")) if path: - self.enemizerCLIpathVar.set(path) + self.widgets[widget].storageVar.set(path) settings["enemizercli"] = path - enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath) - enemizerCLIbrowseButton.pack(side=LEFT) - enemizerPathFrame.pack(fill=X) + # dialog button + self.widgets[widget].pieces["opendialog"] = Button(self.widgets[widget].pieces["frame"], text='...', command=EnemizerSelectPath) + self.widgets[widget].pieces["opendialog"].pack(side=LEFT) + + # frame: pack + self.widgets[widget].pieces["frame"].pack(fill=X) return self,settings diff --git a/source/gui/randomize/generation.py b/source/gui/randomize/generation.py index 376fc564..58eb9b29 100644 --- a/source/gui/randomize/generation.py +++ b/source/gui/randomize/generation.py @@ -2,6 +2,7 @@ from tkinter import ttk, filedialog, StringVar, Button, Entry, Frame, Label, E, import source.gui.widgets as widgets import json import os +from source.classes.Empty import Empty def generation_page(parent,settings): # Generation Setup @@ -28,20 +29,38 @@ def generation_page(parent,settings): self.frames["baserom"].pack(anchor=W, fill=X) ## Locate base ROM # This one's more-complicated, build it and stuff it - baseRomFrame = Frame(self.frames["baserom"]) - baseRomLabel = Label(baseRomFrame, text='Base Rom: ') - self.romVar = StringVar() - romEntry = Entry(baseRomFrame, textvariable=self.romVar) - self.romVar.set(settings["rom"]) + # widget ID + widget = "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(): rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")], initialdir=os.path.join(".")) - self.romVar.set(rom) - romSelectButton = Button(baseRomFrame, text='Select Rom', command=RomSelect) + self.widgets[widget].storageVar.set(rom) + # dialog button + self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"], text='Select Rom', command=RomSelect) - baseRomLabel.pack(side=LEFT) - romEntry.pack(side=LEFT, fill=X, expand=True) - romSelectButton.pack(side=LEFT) - baseRomFrame.pack(fill=X) + # frame label: pack + self.widgets[widget].pieces["frame"].label.pack(side=LEFT) + # textbox: pack + 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 diff --git a/source/gui/randomize/multiworld.py b/source/gui/randomize/multiworld.py index c36854ed..369c1b6d 100644 --- a/source/gui/randomize/multiworld.py +++ b/source/gui/randomize/multiworld.py @@ -2,6 +2,7 @@ from tkinter import ttk, StringVar, Entry, Frame, Label, N, E, W, X, LEFT import source.gui.widgets as widgets import json import os +from source.classes.Empty import Empty def multiworld_page(parent,settings): # Multiworld @@ -26,16 +27,33 @@ def multiworld_page(parent,settings): ## List of Player Names # This one's more-complicated, build it and stuff it - key = "names" - self.widgets[key] = Frame(self.frames["widgets"]) - self.widgets[key].label = Label(self.widgets[key], text='Player names') - self.widgets[key].storageVar = StringVar(value=settings["names"]) + # widget ID + widget = "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): - settings["names"] = self.widgets[key].storageVar.get() - self.widgets[key].storageVar.trace_add("write",saveMultiNames) - self.widgets[key].textbox = Entry(self.widgets[key], textvariable=self.widgets[key].storageVar) - self.widgets[key].label.pack(side=LEFT) - self.widgets[key].textbox.pack(side=LEFT, fill=X, expand=True) - self.widgets[key].pack(anchor=N, fill=X, expand=True) + settings["names"] = self.widgets["names"].storageVar.get() + self.widgets[widget].storageVar.trace_add("write",saveMultiNames) + # textbox + self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], textvariable=self.widgets[widget].storageVar) + + # 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 diff --git a/source/gui/widgets.py b/source/gui/widgets.py index 95e7b1af..f5512c89 100644 --- a/source/gui/widgets.py +++ b/source/gui/widgets.py @@ -1,8 +1,5 @@ from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, RIGHT, X - -# Need a dummy class -class Empty(): - pass +from source.classes.Empty import Empty # Override Spinbox to include mousewheel support for changing value 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 def make_selectbox(self, parent, label, options, storageVar, manager, managerAttrs): - def change_storage(*args): - self.storageVar.set(options[self.labelVar.get()]) - def change_selected(*args): - keys = options.keys() - vals = options.values() - keysList = list(keys) - valsList = list(vals) - self.labelVar.set(keysList[valsList.index(str(self.storageVar.get()))]) self = Frame(parent, name="selectframe-" + label.lower()) - self.storageVar = storageVar - self.storageVar.trace_add("write",change_selected) + + labels = options + + if isinstance(options,dict): + labels = options.keys() + 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.label = Label(self, text=label) + if managerAttrs is not None and "label" in managerAttrs: self.label.pack(managerAttrs["label"]) else: self.label.pack() - self.selectbox = OptionMenu(self, self.labelVar, *options.keys()) + self.selectbox.config(width=20) - self.labelVar.set(managerAttrs["default"] if "default" in managerAttrs else list(options.keys())[0]) + 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: self.selectbox.pack(managerAttrs["selectbox"]) else: @@ -144,6 +195,7 @@ def make_widget_from_dict(self, defn, parent): managerAttrs = defn["managerAttrs"] if "managerAttrs" in defn else None options = defn["options"] if "options" in defn else None widget = make_widget(self, type, parent, label, None, manager, managerAttrs, options) + widget.type = type return widget # Make a set of generic widgets from a dict