diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ff6d8368 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,249 @@ +# workflow name +name: Build + +# fire on +on: [ push, pull_request ] + +# stuff to do +jobs: + # Install & Build + # Set up environment + # Build + # Run build-gui.py + # Run build-dr.py + install-build: + name: Install/Build + # cycle through os list + runs-on: ${{ matrix.os-name }} + + # VM settings + # os & python versions + strategy: + matrix: + os-name: [ ubuntu-latest, ubuntu-16.04, macOS-latest, windows-latest ] + python-version: [ 3.7 ] +# needs: [ install-test ] + steps: + # checkout commit + - name: Checkout commit + uses: actions/checkout@v1 + # install python + - name: Install python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: "x64" + - run: | + python --version + # install dependencies via pip + - name: Install dependencies via pip + env: + OS_NAME: ${{ matrix.os-name }} + run: | + python ./resources/ci/common/install.py + pip install pyinstaller + # try to get UPX + - name: Get UPX + env: + OS_NAME: ${{ matrix.os-name }} + run: | + python ./resources/ci/common/get_upx.py + # run build-gui.py + - name: Build GUI + run: | + python ./build-gui.py + # run build-dr.py + - name: Build DungeonRandomizer + run: | + python ./build-dr.py + # prepare binary artifacts for later step + - name: Prepare Binary Artifacts + env: + OS_NAME: ${{ matrix.os-name }} + run: | + python ./resources/ci/common/prepare_binary.py + # upload binary artifacts for later step + - name: Upload Binary Artifacts + uses: actions/upload-artifact@v1 + with: + name: binaries-${{ matrix.os-name }} + path: ../artifact + + # Install & Preparing Release + # Set up environment + # Local Prepare Release action + install-prepare-release: + name: Install/Prepare Release + # cycle through os list + runs-on: ${{ matrix.os-name }} + + # VM settings + # os & python versions + strategy: + matrix: + # install/release on not xenial + os-name: [ ubuntu-latest, macOS-latest, windows-latest ] + python-version: [ 3.7 ] + + needs: [ install-build ] + steps: + # checkout commit + - name: Checkout commit + uses: actions/checkout@v1 + # install python + - name: Install Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: "x64" + - run: | + python --version + # install dependencies via pip + - name: Install Dependencies via pip + env: + OS_NAME: ${{ matrix.os-name }} + run: | + python ./resources/ci/common/install.py + # download binary artifact + - name: Download Binary Artifact + uses: actions/download-artifact@v1 + with: + name: binaries-${{ matrix.os-name }} + path: ./ + # Prepare AppVersion & Release + - name: Prepare AppVersion & Release + env: + OS_NAME: ${{ matrix.os-name }} + run: | + python ./build-app_version.py + python ./resources/ci/common/prepare_appversion.py + python ./resources/ci/common/prepare_release.py + # upload appversion artifact for later step + - name: Upload AppVersion Artifact + uses: actions/upload-artifact@v1 + with: + name: appversion-${{ matrix.os-name }} + path: ./resources/app/meta/manifests/app_version.txt + # upload archive artifact for later step + - name: Upload Archive Artifact + uses: actions/upload-artifact@v1 + with: + name: archive-${{ matrix.os-name }} + path: ../deploy + + # Deploy to GitHub Releases + # Release Name: ALttPDoorRandomizer v${GITHUB_TAG} + # Release Body: Inline content of RELEASENOTES.md + # Release Body: Fallback to URL to RELEASENOTES.md + # Release Files: ../deploy + deploy-release: + name: Deploy GHReleases + runs-on: ${{ matrix.os-name }} + + # VM settings + # os & python versions + strategy: + matrix: + # release only on bionic + os-name: [ ubuntu-latest ] + python-version: [ 3.7 ] + + needs: [ install-prepare-release ] + steps: + # checkout commit + - name: Checkout commit + uses: actions/checkout@v1 + - name: Install Dependencies via pip + run: | + python -m pip install pytz requests + # download appversion artifact + - name: Download AppVersion Artifact + uses: actions/download-artifact@v1 + with: + name: appversion-${{ matrix.os-name }} + path: ../build + # download ubuntu archive artifact + - name: Download Ubuntu Archive Artifact + uses: actions/download-artifact@v1 + with: + name: archive-ubuntu-latest + path: ../deploy/linux + # download macos archive artifact + - name: Download MacOS Archive Artifact + uses: actions/download-artifact@v1 + with: + name: archive-macOS-latest + path: ../deploy/macos + # download windows archive artifact + - name: Download Windows Archive Artifact + uses: actions/download-artifact@v1 + with: + name: archive-windows-latest + path: ../deploy/windows + # debug info + - name: Debug Info + id: debug_info +# shell: bash +# git tag ${GITHUB_TAG} +# git push origin ${GITHUB_TAG} + run: | + GITHUB_TAG="$(head -n 1 ../build/app_version.txt)" + echo "::set-output name=github_tag::$GITHUB_TAG" + GITHUB_TAG="v${GITHUB_TAG}" + RELEASE_NAME="ALttPDoorRandomizer ${GITHUB_TAG}" + echo "Release Name: ${RELEASE_NAME}" + echo "Git Tag: ${GITHUB_TAG}" + # read releasenotes + - name: Read RELEASENOTES + id: release_notes + run: | + body="$(cat RELEASENOTES.md)" + body="${body//'%'/'%25'}" + body="${body//$'\n'/'%0A'}" + body="${body//$'\r'/'%0D'}" + echo "::set-output name=body::$body" + # create a pre/release + - name: Create a Pre/Release + id: create_release + uses: actions/create-release@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.debug_info.outputs.github_tag }} + release_name: ALttPDoorRandomizer v${{ steps.debug_info.outputs.github_tag }} + body: ${{ steps.release_notes.outputs.body }} + draft: true + prerelease: true + # upload linux archive asset + - name: Upload Linux Archive Asset + id: upload-linux-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ../deploy/linux/ALttPDoorRandomizer.tar.gz + asset_name: ALttPDoorRandomizer-${{ steps.debug_info.outputs.github_tag }}-linux-bionic.tar.gz + asset_content_type: application/gzip + # upload macos archive asset + - name: Upload MacOS Archive Asset + id: upload-macos-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ../deploy/macos/ALttPDoorRandomizer.tar.gz + asset_name: ALttPDoorRandomizer-${{ steps.debug_info.outputs.github_tag }}-osx.tar.gz + asset_content_type: application/gzip + # upload windows archive asset + - name: Upload Windows Archive Asset + id: upload-windows-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ../deploy/windows/ALttPDoorRandomizer.zip + asset_name: ALttPDoorRandomizer-${{ steps.debug_info.outputs.github_tag }}-windows.zip + asset_content_type: application/zip diff --git a/.gitignore b/.gitignore index 91b22ab9..ed3e8193 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,8 @@ RaceRom.py upx/ weights/ -settings.json -working_dirs.json +resources/user/* +!resources/user/.gitkeep *.exe diff --git a/BaseClasses.py b/BaseClasses.py index d16a3c72..437e5544 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,8 +72,13 @@ 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 + if self.mode[player] == "retro": + self.mode[player] = "open" + self.retro[player] = True def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val set_player_attr('_region_cache', {}) @@ -1728,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 58eb06de..eec8327a 100644 --- a/CLI.py +++ b/CLI.py @@ -8,11 +8,10 @@ import textwrap import shlex import sys -from Main import main -from Utils import is_bundled, close_console -from Fill import FillError +import source.classes.constants as CONST +from source.classes.BabelFish import BabelFish -import classes.constants as CONST +from Utils import update_deprecated_args class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): @@ -20,12 +19,15 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): def _get_help_string(self, action): return textwrap.dedent(action.help) -def parse_arguments(argv, no_defaults=False): +def parse_cli(argv, no_defaults=False): def defval(value): return value if not no_defaults else None # get settings - settings = get_settings() + settings = parse_settings() + + lang = "en" + fish = BabelFish(lang=lang) # we need to know how many players we have first parser = argparse.ArgumentParser(add_help=False) @@ -33,265 +35,45 @@ def parse_arguments(argv, no_defaults=False): 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"] = "?" + if arg in settings: + default = settings[arg] + if "type" in argdata and argdata["type"] == "bool": + default = settings[arg] != 0 + argatts["default"] = defval(default) + 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("\\'","'") + else: + argatts["help"] = arghelp + " " + argatts["help"] + 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): @@ -305,7 +87,7 @@ def parse_arguments(argv, no_defaults=False): if multiargs.multi: defaults = copy.deepcopy(ret) for player in range(1, multiargs.multi + 1): - playerargs = parse_arguments(shlex.split(getattr(ret,f"p{player}")), True) + playerargs = parse_cli(shlex.split(getattr(ret,f"p{player}")), True) for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', @@ -323,9 +105,10 @@ def parse_arguments(argv, no_defaults=False): return ret -def get_settings(): +def parse_settings(): # set default settings settings = { + "lang": "en", "retro": False, "mode": "open", "logic": "noglitches", @@ -341,7 +124,7 @@ def get_settings(): "algorithm": "balanced", "openpyramid": False, - "shuffleganon": False, + "shuffleganon": True, "shuffle": "vanilla", "shufflepots": False, @@ -368,100 +151,103 @@ def get_settings(): "quickswap": False, "heartcolor": "red", "heartbeep": "normal", - "sprite": None, + "sprite": os.path.join(".","data","sprites","official","001.link.1.zspr"), "fastmenu": "normal", "ow_palettes": "default", "uw_palettes": "default", "create_spoiler": False, "skip_playthrough": False, + "calc_playthrough": True, "suppress_rom": False, + "create_rom": True, "usestartinventory": False, "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": {} } @@ -477,11 +263,14 @@ def get_settings(): settings[k] = v return settings - +# Priority fallback is: +# 1: CLI +# 2: Settings file +# 3: Canned defaults def get_args_priority(settings_args, gui_args, cli_args): args = {} - args["settings"] = get_settings() if settings_args is None else settings_args - args["gui"] = {} if gui_args is None else gui_args + args["settings"] = parse_settings() if settings_args is None else settings_args + args["gui"] = gui_args args["cli"] = cli_args args["load"] = args["settings"] @@ -492,17 +281,38 @@ def get_args_priority(settings_args, gui_args, cli_args): if args["cli"] is None: args["cli"] = {} - cli = vars(parse_arguments(None)) + cli = vars(parse_cli(None)) for k, v in cli.items(): if isinstance(v, dict) and 1 in v: args["cli"][k] = v[1] else: args["cli"][k] = v - load_doesnt_have_key = k not in args["load"] - different_val = (k in args["load"] and k in args["cli"]) and (args["load"][k] != args["cli"][k]) - cli_has_empty_dict = k in args["cli"] and isinstance(args["cli"][k], dict) and len(args["cli"][k]) == 0 - if load_doesnt_have_key or different_val: - if not cli_has_empty_dict: - args["load"][k] = args["cli"][k] + args["cli"] = argparse.Namespace(**args["cli"]) + + cli = vars(args["cli"]) + for k in vars(args["cli"]): + load_doesnt_have_key = k not in args["load"] + cli_val = cli[k] + if isinstance(cli_val,dict) and 1 in cli_val: + cli_val = cli_val[1] + different_val = (k in args["load"] and k in cli) and (str(args["load"][k]) != str(cli_val)) + cli_has_empty_dict = k in cli and isinstance(cli_val, dict) and len(cli_val) == 0 + if load_doesnt_have_key or different_val: + if not cli_has_empty_dict: + args["load"][k] = cli_val + + newArgs = {} + for key in [ "settings", "gui", "cli", "load" ]: + if args[key]: + if isinstance(args[key],dict): + newArgs[key] = argparse.Namespace(**args[key]) + else: + newArgs[key] = args[key] + + newArgs[key] = update_deprecated_args(newArgs[key]) + else: + newArgs[key] = args[key] + + args = newArgs return args diff --git a/DoorShuffle.py b/DoorShuffle.py index 669f1225..90c572ce 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -318,7 +318,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) @@ -328,15 +328,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] @@ -370,7 +370,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_ last_key = builder.name loops += 1 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, entrances_map, connections, potentials, enabled_entrances, world, player) ds.name = name @@ -528,7 +528,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. @@ -698,7 +698,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) @@ -819,7 +819,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): @@ -976,14 +976,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() @@ -1003,7 +1003,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 a57e9f85..34b2ba9d 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1151,8 +1151,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 @@ -1436,9 +1436,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(): @@ -1476,7 +1476,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) @@ -1525,11 +1525,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 @@ -1833,9 +1833,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) @@ -1846,10 +1846,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) @@ -1862,8 +1862,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..15e73f85 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -8,14 +8,16 @@ import textwrap import shlex import sys -from CLI import parse_arguments -from Main import main +from source.classes.BabelFish import BabelFish + +from CLI import parse_cli, get_args_priority +from Main import main, EnemizerError from Rom import get_sprite_from_name from Utils import is_bundled, close_console from Fill import FillError def start(): - args = parse_arguments(None) + args = parse_cli(None) if is_bundled() and len(sys.argv) == 1: # for the bundled builds, if we have no arguments, the user @@ -42,20 +44,27 @@ def start(): loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel] logging.basicConfig(format='%(message)s', level=loglevel) + priority = get_args_priority(None, None, args) + lang = "en" + if "load" in priority and "lang" in priority["load"]: + lang = priority["load"].lang + fish = BabelFish(lang=lang) + if args.gui: from Gui import guiMain guiMain(args) - elif args.count is not None: + elif args.count is not None and args.count > 1: + random.seed(None) seed = args.seed or random.randint(0, 999999999) failures = [] logger = logging.getLogger('') for _ in range(args.count): try: - main(seed=seed, args=args) - logger.info('Finished run %s', _+1) - except (FillError, Exception, RuntimeError) as err: + main(seed=seed, args=args, fish=fish) + logger.info('%s %s', fish.translate("cli","cli","finished.run"), _+1) + except (FillError, EnemizerError, 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 +75,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/DungeonRandomizer.spec b/DungeonRandomizer.spec index 7b8de387..163e8413 100644 --- a/DungeonRandomizer.spec +++ b/DungeonRandomizer.spec @@ -1,5 +1,7 @@ # -*- mode: python -*- +import sys + block_cipher = None console = True @@ -21,10 +23,14 @@ def recurse_for_py_files(names_so_far): return returnvalue hiddenimports = [] +binaries = [] + +#if sys.platform.find("windows"): +# binaries.append(("ucrtbase.dll",".")) a = Analysis(['DungeonRandomizer.py'], pathex=[], - binaries=[], + binaries=binaries, datas=[], hiddenimports=hiddenimports, hookspath=[], diff --git a/Fill.py b/Fill.py index e38d757d..84a6cf6d 100644 --- a/Fill.py +++ b/Fill.py @@ -392,7 +392,7 @@ def balance_multiworld_progression(world): threshold = max(reachable_locations_count.values()) - 20 balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] - if balancing_players: + if balancing_players is not None and len(balancing_players) > 0: balancing_state = state.copy() balancing_unchecked_locations = unchecked_locations.copy() balancing_reachables = reachable_locations_count.copy() diff --git a/Gui.py b/Gui.py index 632b9a43..66fd4999 100755 --- a/Gui.py +++ b/Gui.py @@ -4,26 +4,29 @@ import os import sys from tkinter import Tk, Button, BOTTOM, TOP, StringVar, BooleanVar, X, BOTH, RIGHT, ttk, messagebox -from argparse import Namespace -from CLI import get_settings, get_args_priority -from DungeonRandomizer import parse_arguments -from gui.adjust.overview import adjust_page -from gui.startinventory.overview import startinventory_page -from gui.custom.overview import custom_page -from gui.loadcliargs import loadcliargs, loadadjustargs -from gui.randomize.item import item_page -from gui.randomize.entrando import entrando_page -from gui.randomize.enemizer import enemizer_page -from gui.randomize.dungeon import dungeon_page -from gui.randomize.multiworld import multiworld_page -from gui.randomize.gameoptions import gameoptions_page -from gui.randomize.generation import generation_page -from gui.bottom import bottom_frame, create_guiargs +from CLI import get_args_priority +from DungeonRandomizer import parse_cli +from source.gui.adjust.overview import adjust_page +from source.gui.startinventory.overview import startinventory_page +from source.gui.custom.overview import custom_page +from source.gui.loadcliargs import loadcliargs, loadadjustargs +from source.gui.randomize.item import item_page +from source.gui.randomize.entrando import entrando_page +from source.gui.randomize.enemizer import enemizer_page +from source.gui.randomize.dungeon import dungeon_page +#from source.gui.randomize.multiworld import multiworld_page +from source.gui.randomize.gameoptions import gameoptions_page +from source.gui.randomize.generation import generation_page +from source.gui.bottom import bottom_frame, create_guiargs from GuiUtils import set_icon from Main import __version__ as ESVersion +from source.classes.BabelFish import BabelFish +from source.classes.Empty import Empty + def guiMain(args=None): + # Save settings to file def save_settings(args): user_resources_path = os.path.join(".", "resources", "user") settings_path = os.path.join(user_resources_path) @@ -35,6 +38,7 @@ def guiMain(args=None): f.write(json.dumps(args, indent=2)) os.chmod(os.path.join(settings_path, "settings.json"),0o755) + # Save settings from GUI def save_settings_from_gui(confirm): gui_args = vars(create_guiargs(self)) if self.randomSprite.get(): @@ -73,9 +77,13 @@ def guiMain(args=None): # get args # getting Settings & CLI (no GUI built yet) self.args = get_args_priority(None, None, None) + lang = "en" + if "load" in self.args and "lang" in self.args["load"]: + lang = self.args["load"].lang + self.fish = BabelFish(lang=lang) # get saved settings - self.settings = self.args["settings"] + self.settings = vars(self.args["settings"]) # make array for pages self.pages = {} @@ -83,6 +91,7 @@ def guiMain(args=None): # make array for frames self.frames = {} + # make pages for each section self.notebook = ttk.Notebook(self) self.pages["randomizer"] = ttk.Frame(self.notebook) self.pages["adjust"] = ttk.Frame(self.notebook) @@ -127,8 +136,8 @@ def guiMain(args=None): self.pages["randomizer"].notebook.add(self.pages["randomizer"].pages["dungeon"], text="Dungeon Shuffle") # Multiworld - self.pages["randomizer"].pages["multiworld"],self.settings = multiworld_page(self.pages["randomizer"].notebook,self.settings) - self.pages["randomizer"].notebook.add(self.pages["randomizer"].pages["multiworld"], text="Multiworld") +# self.pages["randomizer"].pages["multiworld"],self.settings = multiworld_page(self.pages["randomizer"].notebook,self.settings) +# self.pages["randomizer"].notebook.add(self.pages["randomizer"].pages["multiworld"], text="Multiworld") # Game Options self.pages["randomizer"].pages["gameoptions"] = gameoptions_page(self, self.pages["randomizer"].notebook) @@ -142,13 +151,15 @@ def guiMain(args=None): self.pages["randomizer"].notebook.pack() # 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() @@ -178,9 +189,10 @@ def guiMain(args=None): # load adjust settings into options loadadjustargs(self, self.settings) + # run main window mainWindow.mainloop() if __name__ == '__main__': - args = parse_arguments(None) + args = parse_cli(None) guiMain(args) diff --git a/Gui.spec b/Gui.spec index cd6de67d..a1b1a86c 100644 --- a/Gui.spec +++ b/Gui.spec @@ -1,8 +1,13 @@ # -*- mode: python -*- +import sys + block_cipher = None console = True +if sys.platform.find("mac") or sys.platform.find("osx"): + console = False + def recurse_for_py_files(names_so_far): returnvalue = [] for name in os.listdir(os.path.join(*names_so_far)): @@ -21,10 +26,14 @@ def recurse_for_py_files(names_so_far): return returnvalue hiddenimports = [] +binaries = [] -a = Analysis(['Gui.py'], +#if sys.platform.find("windows"): +# binaries.append(("ucrtbase.dll",".")) + +a = Analysis(['DungeonRandomizer.py'], pathex=[], - binaries=[], + binaries=binaries, datas=[], hiddenimports=hiddenimports, hookspath=[], diff --git a/ItemList.py b/ItemList.py index 43c24f11..27c0b384 100644 --- a/ItemList.py +++ b/ItemList.py @@ -9,7 +9,7 @@ from EntranceShuffle import connect_entrance from Fill import FillError, fill_restrictive from Items import ItemFactory -import classes.constants as CONST +import source.classes.constants as CONST #This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. @@ -126,6 +126,7 @@ difficulties = { ), } +# Translate between Mike's label array and YAML/JSON keys def get_custom_array_key(item): label_switcher = { "silverarrow": "silversupgrade", @@ -257,17 +258,17 @@ def generate_itempool(world, player): # set up item pool if world.custom: (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray) - world.rupoor_cost = min(world.customitemarray["rupoorcost"], 9999) + world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999) else: (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.doorShuffle[player]) if player in world.pool_adjustment.keys(): amt = world.pool_adjustment[player] if amt < 0: - for i in range(0, amt): + for _ in range(0, amt): pool.remove('Rupees (20)') elif amt > 0: - for i in range(0, amt): + for _ in range(0, amt): pool.append('Rupees (20)') for item in precollected_items: @@ -321,9 +322,9 @@ def generate_itempool(world, player): # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) - if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray["heartcontainer"] == 0): + if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0): [item for item in items if item.name == 'Boss Heart Container'][0].advancement = True - elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray["heartpiece"] < 4): + elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4): adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4] for hp in adv_heart_pieces: hp.advancement = True @@ -600,6 +601,8 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray): + if isinstance(customitemarray,dict) and 1 in customitemarray: + customitemarray = customitemarray[1] pool = [] placed_items = {} precollected_items = [] @@ -697,7 +700,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode if itemtotal < total_items_to_place: nothings = total_items_to_place - itemtotal - print("Placing " + str(nothings) + " Nothings") +# print("Placing " + str(nothings) + " Nothings") pool.extend(['Nothing'] * nothings) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) @@ -707,24 +710,25 @@ def test(): for difficulty in ['normal', 'hard', 'expert']: for goal in ['ganon', 'triforcehunt', 'pedestal']: for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']: - for mode in ['open', 'standard', 'inverted']: + for mode in ['open', 'standard', 'inverted', 'retro']: for swords in ['random', 'assured', 'swordless', 'vanilla']: for progressive in ['on', 'off']: for shuffle in ['full', 'insanity_legacy']: for retro in [True, False]: - out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro) - count = len(out[0]) + len(out[1]) + for door_shuffle in ['basic', 'crossed', 'vanilla']: + out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, door_shuffle) + count = len(out[0]) + len(out[1]) - correct_count = total_items_to_place - if goal == 'pedestal' and swords != 'vanilla': - # pedestal goals generate one extra item - correct_count += 1 - if retro: - correct_count += 28 - try: - assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro)) - except AssertionError as e: - print(e) + correct_count = total_items_to_place + if goal == 'pedestal' and swords != 'vanilla': + # pedestal goals generate one extra item + correct_count += 1 + if retro: + correct_count += 28 + try: + assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro)) + except AssertionError as e: + print(e) if __name__ == '__main__': test() diff --git a/Main.py b/Main.py index 8c562078..5085c935 100644 --- a/Main.py +++ b/Main.py @@ -19,15 +19,17 @@ from Doors import create_doors from DoorShuffle import link_doors from RoomData import create_rooms from Rules import set_rules -from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive +from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive, dungeon_regions from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression from ItemList import generate_itempool, difficulties, fill_prizes -from Utils import output_path, parse_player_names +from Utils import output_path, parse_player_names, print_wiki_doors_by_region, print_wiki_doors_by_room -__version__ = '0.0.19dev' +__version__ = '0.0.20dev' +class EnemizerError(RuntimeError): + pass -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 +61,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) @@ -77,7 +84,8 @@ def main(args, seed=None): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none': - world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it + if hasattr(world,"escape_assist") and player in world.escape_assist: + world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it for tok in filter(None, args.startinventory[player].split(',')): item = ItemFactory(tok.strip(), player) @@ -94,7 +102,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': @@ -102,7 +110,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) @@ -110,21 +118,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()) + @@ -138,9 +146,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 @@ -159,23 +175,23 @@ 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!') - - logger.info('Patching ROM.') + raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed) rom_names = [] jsonout = {} if not args.suppress_rom: + logger.info(world.fish.translate("cli","cli","patching.rom")) for team in range(world.teams): for player in range(1, world.players + 1): sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit' + enemized = False use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or args.shufflepots[player] or sprite_random_on_hit) @@ -185,13 +201,20 @@ def main(args, seed=None): patch_rom(world, rom, player, team, use_enemizer) if use_enemizer and (args.enemizercli or not args.jsonout): + if args.rom and not(os.path.isfile(args.rom)): + raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom) if os.path.exists(args.enemizercli): patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit) + enemized = True 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.") + enemizerMsg = world.fish.translate("cli","cli","enemizer.not.found") + ': ' + args.enemizercli + "\n" + enemizerMsg += world.fish.translate("cli","cli","enemizer.nothing.applied") + logging.warning(enemizerMsg) + raise EnemizerError(enemizerMsg) + + patch_rom(world, rom, player, team, enemized) if args.race: patch_race_rom(rom) @@ -241,20 +264,29 @@ def main(args, seed=None): with open(output_path('%s_multidata' % outfilebase), 'wb') as f: f.write(multidata) - if args.create_spoiler and not args.jsonout: - 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: print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()})) - elif args.create_spoiler and not args.skip_playthrough: + elif args.create_spoiler: + logger.info(world.fish.translate("cli","cli","patching.spoiler")) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) - logger.info('Done. Enjoy.') - logger.info('Total Time: %s', time.perf_counter() - start) + YES = world.fish.translate("cli","cli","yes") + NO = world.fish.translate("cli","cli","no") + logger.info("") + logger.info(world.fish.translate("cli","cli","done")) + logger.info("") + logger.info(world.fish.translate("cli","cli","made.rom") % (YES if (args.create_rom) else NO)) + logger.info(world.fish.translate("cli","cli","made.playthrough") % (YES if (args.calc_playthrough) else NO)) + logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and args.create_spoiler) else NO)) + 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) return world @@ -383,7 +415,7 @@ def copy_dynamic_regions_and_locations(world, ret): new_loc.always_allow = location.always_allow new_loc.item_rule = location.item_rule new_reg.locations.append(new_loc) - + ret.clear_location_cache() @@ -398,7 +430,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() @@ -417,11 +449,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 @@ -473,9 +505,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/Mystery.py b/Mystery.py index d175be3e..f42123fd 100644 --- a/Mystery.py +++ b/Mystery.py @@ -5,7 +5,7 @@ import urllib.request import urllib.parse import re -from DungeonRandomizer import parse_arguments +from DungeonRandomizer import parse_cli from Main import main as DRMain def parse_yaml(txt): @@ -71,7 +71,7 @@ def main(): weights_cache[path] = get_weights(path) print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}") - erargs = parse_arguments(['--multi', str(args.multi)]) + erargs = parse_cli(['--multi', str(args.multi)]) erargs.seed = seed erargs.names = args.names erargs.create_spoiler = args.create_spoiler diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 00000000..cd41b205 --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,6 @@ +# Features + +## Door Randomizer + +* Native GUI executables +* Native Dungeon Randomizer CLI executables diff --git a/Rom.py b/Rom.py index 61380543..0d942f35 100644 --- a/Rom.py +++ b/Rom.py @@ -78,6 +78,8 @@ class LocalRom(object): self.name = name self.hash = hash self.orig_buffer = None + if not os.path.isfile(file): + raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file) with open(file, 'rb') as stream: self.buffer = read_rom(stream) if patch: @@ -759,10 +761,10 @@ def patch_rom(world, rom, player, team, enemized): difficulty.progressive_shield_limit, overflow_replacement, difficulty.progressive_armor_limit, overflow_replacement, difficulty.progressive_bottle_limit, overflow_replacement]) - + #Work around for json patch ordering issues - write bow limit separately so that it is replaced in the patch rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement]) - + if difficulty.progressive_bow_limit < 2 and world.swords == 'swordless': rom.write_bytes(0x180098, [2, overflow_replacement]) rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon @@ -2089,9 +2091,9 @@ def patch_shuffled_dark_sanc(world, rom, player): dark_sanc_entrance = str(world.get_region('Inverted Dark Sanctuary', player).entrances[0].name) room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] door_index = door_addresses[str(dark_sanc_entrance)][0] - + rom.write_byte(0x180241, 0x01) - rom.write_byte(0x180248, door_index + 1) + rom.write_byte(0x180248, door_index + 1) write_int16(rom, 0x180250, room_id) rom.write_byte(0x180252, ow_area) write_int16s(rom, 0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x]) diff --git a/Utils.py b/Utils.py index 938ed097..63670d78 100644 --- a/Utils.py +++ b/Utils.py @@ -78,12 +78,16 @@ def output_path(path): NSUserDomainMask = 1 # True for expanding the tilde into a fully qualified path documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0] + elif sys.platform.find("linux") or sys.platform.find("ubuntu") or sys.platform.find("unix"): + documents = os.path.join(os.path.expanduser("~"),"Documents") else: raise NotImplementedError('Not supported yet') - output_path.cached_path = os.path.join(documents, 'ALttPEntranceRandomizer') + output_path.cached_path = os.path.join(documents, 'ALttPDoorRandomizer') if not os.path.exists(output_path.cached_path): - os.mkdir(output_path.cached_path) + os.makedirs(output_path.cached_path) + if not os.path.join(output_path.cached_path, path): + os.makedirs(os.path.join(output_path.cached_path, path)) return os.path.join(output_path.cached_path, path) output_path.cached_path = None @@ -203,8 +207,7 @@ def read_entrance_data(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan) print(string) -def print_wiki_doors(d_regions, world, player): - +def print_wiki_doors_by_region(d_regions, world, player): for d, region_list in d_regions.items(): tile_map = {} for region in region_list: @@ -219,28 +222,102 @@ def print_wiki_doors(d_regions, world, player): if tile not in tile_map: tile_map[tile] = [] tile_map[tile].append(r) - print(d) - print('{| class="wikitable"') - print('|-') - print('! Room') - print('! Supertile') - print('! Doors') + toprint = "" + toprint += ('') + "\n" + toprint += ('== Room List ==') + "\n" + toprint += "\n" + toprint += ('{| class="wikitable"') + "\n" + toprint += ('|-') + "\n" + toprint += ('! Room !! Supertile !! Doors') + "\n" for tile, region_list in tile_map.items(): tile_done = False for region in region_list: - print('|-') - print('| '+region.name) + toprint += ('|-') + "\n" + toprint += ('| {{Dungeon Room|{{PAGENAME}}|' + region.name + '}}') + "\n" if not tile_done: listlen = len(region_list) link = '| {{UnderworldMapLink|'+str(tile)+'}}' - print(link if listlen < 2 else '| rowspan = '+str(listlen)+' '+link) + toprint += (link if listlen < 2 else '| rowspan = '+str(listlen)+' '+link) + "\n" tile_done = True strs_to_print = [] for ext in region.exits: - strs_to_print.append(ext.name) - print('| '+'
'.join(strs_to_print)) - print('|}') + strs_to_print.append('{{Dungeon Door|{{PAGENAME}}|' + ext.name + '}}') + toprint += ('| '+'
'.join(strs_to_print)) + toprint += "\n" + toprint += ('|}') + "\n" + with open(os.path.join(".","resources", "user", "regions-" + d + ".txt"),"w+") as f: + f.write(toprint) +def update_deprecated_args(args): + argVars = vars(args) + truthy = [ 1, True, "True", "true" ] + # Don't do: Yes + # Do: No + if "suppress_rom" in argVars: + args.create_rom = args.suppress_rom not in truthy + # Don't do: No + # Do: Yes + if "create_rom" in argVars: + args.suppress_rom = not args.create_rom in truthy + + # Don't do: Yes + # Do: No + if "no_shuffleganon" in argVars: + args.shuffleganon = not args.no_shuffleganon in truthy + # Don't do: No + # Do: Yes + if "shuffleganon" in argVars: + args.no_shuffleganon = not args.shuffleganon in truthy + + # Don't do: Yes + # Do: No + if "skip_playthrough" in argVars: + args.calc_playthrough = not args.skip_playthrough in truthy + # Don't do: No + # Do: Yes + if "calc_playthrough" in argVars: + args.skip_playthrough = not args.calc_playthrough in truthy + + return args + +def print_wiki_doors_by_room(d_regions, world, player): + for d, region_list in d_regions.items(): + tile_map = {} + for region in region_list: + tile = None + r = world.get_region(region, player) + for ext in r.exits: + door = world.check_for_door(ext.name, player) + if door is not None and door.roomIndex != -1: + tile = door.roomIndex + break + if tile is not None: + if tile not in tile_map: + tile_map[tile] = [] + tile_map[tile].append(r) + toprint = "" + toprint += ('') + "\n" + for tile, region_list in tile_map.items(): + for region in region_list: + toprint += ('') + "\n" + toprint += ('{{Infobox dungeon room') + "\n" + toprint += ('| dungeon = {{ROOTPAGENAME}}') + "\n" + toprint += ('| supertile = ' + str(tile)) + "\n" + toprint += ('| tile = x') + "\n" + toprint += ('}}') + "\n" + toprint += ('') + "\n" + toprint += ('== Doors ==') + "\n" + toprint += ('{| class="wikitable"') + "\n" + toprint += ('|-') + "\n" + toprint += ('! Door !! Room Side !! Requirement') + "\n" + for ext in region.exits: + ext_part = ext.name.replace(region.name,'') + ext_part = ext_part.strip() + toprint += ('{{DungeonRoomDoorList/Row|{{ROOTPAGENAME}}|{{SUBPAGENAME}}|' + ext_part + '|Side|}}') + "\n" + toprint += ('|}') + "\n" + toprint += ('') + "\n" + with open(os.path.join(".","resources", "user", "rooms-" + d + ".txt"),"w+") as f: + f.write(toprint) def print_xml_doors(d_regions, world, player): root = ET.Element('root') diff --git a/build-app_version.py b/build-app_version.py new file mode 100644 index 00000000..6d63f3ca --- /dev/null +++ b/build-app_version.py @@ -0,0 +1,5 @@ +from Main import __version__ as DRVersion +import os + +with(open(os.path.join("resources","app","meta","manifests","app_version.txt"),"w+")) as f: + f.write(DRVersion) diff --git a/build-dr.py b/build-dr.py index 120028d9..08c1ce93 100644 --- a/build-dr.py +++ b/build-dr.py @@ -1,17 +1,21 @@ import subprocess import os import shutil +import sys +# Destination is current dir DEST_DIRECTORY = '.' +# Check for UPX if os.path.isdir("upx"): upx_string = "--upx-dir=upx" else: upx_string = "" -if os.path.isdir("build"): +if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.find("osx"): shutil.rmtree("build") +# Run pyinstaller for DungeonRandomizer subprocess.run(" ".join(["pyinstaller DungeonRandomizer.spec ", upx_string, "-y ", diff --git a/build-gui.py b/build-gui.py index 3f63548d..ff8ccc90 100644 --- a/build-gui.py +++ b/build-gui.py @@ -1,17 +1,21 @@ import subprocess import os import shutil +import sys +# Destination is current dir DEST_DIRECTORY = '.' +# Check for UPX if os.path.isdir("upx"): upx_string = "--upx-dir=upx" else: upx_string = "" -if os.path.isdir("build"): +if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.find("osx"): shutil.rmtree("build") +# Run pyinstaller for Gui subprocess.run(" ".join(["pyinstaller Gui.spec ", upx_string, "-y ", diff --git a/classes/__init__.py b/classes/__init__.py deleted file mode 100644 index a9d60931..00000000 --- a/classes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "classes" package diff --git a/gui/__init__.py b/gui/__init__.py deleted file mode 100644 index 8c5232bc..00000000 --- a/gui/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "gui" package diff --git a/gui/about/__init__.py b/gui/about/__init__.py deleted file mode 100644 index 1ad9a517..00000000 --- a/gui/about/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "gui.about" package diff --git a/gui/adjust/__init__.py b/gui/adjust/__init__.py deleted file mode 100644 index 3e1ae764..00000000 --- a/gui/adjust/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "gui.adjust" package diff --git a/gui/bottom.py b/gui/bottom.py deleted file mode 100644 index 22d94d35..00000000 --- a/gui/bottom.py +++ /dev/null @@ -1,147 +0,0 @@ -from tkinter import ttk, messagebox, StringVar, Button, Entry, Frame, Label, Spinbox, E, W, LEFT, RIGHT, X -from argparse import Namespace -from functools import partial -import logging -import os -import random -from CLI import parse_arguments, get_settings -from Main import main -from Utils import local_path, output_path, open_file -import classes.constants as CONST -import gui.widgets as widgets - - -def bottom_frame(self, parent, args=None): - # Bottom Frame - self = ttk.Frame(parent) - - # Bottom Frame options - self.widgets = {} - - seedCountFrame = Frame(self) - seedCountFrame.pack() - ## Seed # - seedLabel = Label(self, text='Seed #') - savedSeed = parent.settings["seed"] - self.seedVar = StringVar(value=savedSeed) - def saveSeed(caller,_,mode): - savedSeed = self.seedVar.get() - parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None - self.seedVar.trace_add("write",saveSeed) - seedEntry = Entry(self, width=15, textvariable=self.seedVar) - seedLabel.pack(side=LEFT) - seedEntry.pack(side=LEFT) - - ## Number of Generation attempts - key = "generationcount" - self.widgets[key] = widgets.make_widget( - self, - "spinbox", - self, - "Count", - None, - None, - {"label": {"side": LEFT}, "spinbox": {"side": RIGHT}} - ) - self.widgets[key].pack(side=LEFT) - - def generateRom(): - guiargs = create_guiargs(parent) - # get default values for missing parameters - for k,v in vars(parse_arguments(['--multi', str(guiargs.multi)])).items(): - if k not in vars(guiargs): - setattr(guiargs, k, v) - elif type(v) is dict: # use same settings for every player - setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)}) - try: - if guiargs.count is not None: - seed = guiargs.seed - for _ in range(guiargs.count): - main(seed=seed, args=guiargs) - seed = random.randint(0, 999999999) - else: - main(seed=guiargs.seed, args=guiargs) - except Exception as e: - logging.exception(e) - messagebox.showerror(title="Error while creating seed", message=str(e)) - else: - messagebox.showinfo(title="Success", message="Rom patched successfully") - - ## Generate Button - generateButton = Button(self, text='Generate Patched Rom', command=generateRom) - generateButton.pack(side=LEFT) - - def open_output(): - if args and args.outputpath: - open_file(output_path(args.outputpath)) - else: - open_file(output_path(parent.settings["outputpath"])) - - openOutputButton = Button(self, text='Open Output Directory', command=open_output) - openOutputButton.pack(side=RIGHT) - - ## Documentation Button - if os.path.exists(local_path('README.html')): - def open_readme(): - open_file(local_path('README.html')) - openReadmeButton = Button(self, text='Open Documentation', command=open_readme) - openReadmeButton.pack(side=RIGHT) - - return self - - -def create_guiargs(parent): - guiargs = Namespace() - - # set up settings to gather - # Page::Subpage::GUI-id::param-id - options = CONST.SETTINGSTOPROCESS - - for mainpage in options: - for subpage in options[mainpage]: - for widget in options[mainpage][subpage]: - arg = options[mainpage][subpage][widget] - setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get()) - - guiargs.enemizercli = parent.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.get() - - guiargs.multi = int(parent.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.get()) - - guiargs.rom = parent.pages["randomizer"].pages["generation"].romVar.get() - guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get()) - - guiargs.seed = int(parent.frames["bottom"].seedVar.get()) if parent.frames["bottom"].seedVar.get() else None - guiargs.count = int(parent.frames["bottom"].widgets["generationcount"].storageVar.get()) if parent.frames["bottom"].widgets["generationcount"].storageVar.get() != '1' else None - - adjustargs = { - "nobgm": "disablemusic", - "quickswap": "quickswap", - "heartcolor": "heartcolor", - "heartbeep": "heartbeep", - "menuspeed": "fastmenu", - "owpalettes": "ow_palettes", - "uwpalettes": "uw_palettes" - } - for adjustarg in adjustargs: - internal = adjustargs[adjustarg] - setattr(guiargs,"adjust." + internal, parent.pages["adjust"].content.widgets[adjustarg].storageVar.get()) - - customitems = CONST.CUSTOMITEMS - guiargs.startinventory = [] - guiargs.customitemarray = {} - guiargs.startinventoryarray = {} - for customitem in customitems: - if customitem not in ["triforcepiecesgoal", "triforce", "rupoor", "rupoorcost"]: - amount = int(parent.pages["startinventory"].content.startingWidgets[customitem].storageVar.get()) - guiargs.startinventoryarray[customitem] = amount - for i in range(0, amount): - label = CONST.CUSTOMITEMLABELS[customitems.index(customitem)] - guiargs.startinventory.append(label) - guiargs.customitemarray[customitem] = int(parent.pages["custom"].content.customWidgets[customitem].storageVar.get()) - - guiargs.startinventory = ','.join(guiargs.startinventory) - - guiargs.sprite = parent.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"] - guiargs.randomSprite = parent.randomSprite.get() - guiargs.outputpath = parent.outputPath.get() - return guiargs diff --git a/gui/custom/__init__.py b/gui/custom/__init__.py deleted file mode 100644 index 33cec31c..00000000 --- a/gui/custom/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "gui.custom" package diff --git a/gui/loadcliargs.py b/gui/loadcliargs.py deleted file mode 100644 index 846daaa8..00000000 --- a/gui/loadcliargs.py +++ /dev/null @@ -1,74 +0,0 @@ -from classes.SpriteSelector import SpriteSelector as spriteSelector -from gui.randomize.gameoptions import set_sprite -from Rom import Sprite, get_sprite_from_name -import classes.constants as CONST - -def loadcliargs(gui, args, settings=None): - if args is not None: -# for k, v in vars(args).items(): -# if type(v) is dict: -# setattr(args, k, v[1]) # only get values for player 1 for now - # load values from commandline args - - # set up options to get - # Page::Subpage::GUI-id::param-id - options = CONST.SETTINGSTOPROCESS - - for mainpage in options: - for subpage in options[mainpage]: - for widget in options[mainpage][subpage]: - arg = options[mainpage][subpage][widget] - gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg]) - if subpage == "gameoptions" and not widget == "hints": - hasSettings = settings is not None - hasWidget = ("adjust." + widget) in settings if hasSettings else None - if hasWidget is None: - gui.pages["adjust"].content.widgets[widget].storageVar.set(args[arg]) - - gui.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.set(args["enemizercli"]) - gui.pages["randomizer"].pages["generation"].romVar.set(args["rom"]) - - if args["multi"]: - gui.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.set(str(args["multi"])) - if args["seed"]: - gui.frames["bottom"].seedVar.set(str(args["seed"])) - if args["count"]: - gui.frames["bottom"].widgets["generationcount"].storageVar.set(str(args["count"])) - gui.outputPath.set(args["outputpath"]) - - def sprite_setter(spriteObject): - gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"] = spriteObject - if args["sprite"] is not None: - sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"]) - set_sprite(sprite_obj, False, spriteSetter=sprite_setter, - spriteNameVar=gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteNameVar"], - randomSpriteVar=gui.randomSprite) - - def sprite_setter_adj(spriteObject): - gui.pages["adjust"].content.sprite = spriteObject - if args["sprite"] is not None: - sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"]) - set_sprite(sprite_obj, False, spriteSetter=sprite_setter_adj, - spriteNameVar=gui.pages["adjust"].content.spriteNameVar2, - randomSpriteVar=gui.randomSprite) - -def loadadjustargs(gui, settings): - options = { - "adjust": { - "content": { - "nobgm": "adjust.nobgm", - "quickswap": "adjust.quickswap", - "heartcolor": "adjust.heartcolor", - "heartbeep": "adjust.heartbeep", - "menuspeed": "adjust.menuspeed", - "owpalettes": "adjust.owpalettes", - "uwpalettes": "adjust.uwpalettes" - } - } - } - for mainpage in options: - for subpage in options[mainpage]: - for widget in options[mainpage][subpage]: - key = options[mainpage][subpage][widget] - if key in settings: - gui.pages[mainpage].content.widgets[widget].storageVar.set(settings[key]) diff --git a/gui/randomize/__init__.py b/gui/randomize/__init__.py deleted file mode 100644 index ecf3a271..00000000 --- a/gui/randomize/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "gui.randomize" package diff --git a/gui/randomize/enemizer.py b/gui/randomize/enemizer.py deleted file mode 100644 index cb78281c..00000000 --- a/gui/randomize/enemizer.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -from tkinter import ttk, filedialog, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, LabelFrame, OptionMenu, N, E, W, LEFT, RIGHT, BOTTOM, X -import gui.widgets as widgets -import json -import os -import webbrowser - -def enemizer_page(parent,settings): - def open_enemizer_download(_evt): - webbrowser.open("https://github.com/Bonta0/Enemizer/releases") - - # Enemizer - self = ttk.Frame(parent) - - # Enemizer options - self.widgets = {} - - # Enemizer option sections - self.frames = {} - - self.frames["checkboxes"] = Frame(self) - self.frames["checkboxes"].pack(anchor=W) - - self.frames["selectOptionsFrame"] = Frame(self) - self.frames["leftEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"]) - self.frames["rightEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"]) - self.frames["bottomEnemizerFrame"] = Frame(self) - self.frames["selectOptionsFrame"].pack(fill=X) - self.frames["leftEnemizerFrame"].pack(side=LEFT) - self.frames["rightEnemizerFrame"].pack(side=RIGHT) - self.frames["bottomEnemizerFrame"].pack(fill=X) - - with open(os.path.join("resources","app","gui","randomize","enemizer","widgets.json")) as widgetDefns: - myDict = json.load(widgetDefns) - for framename,theseWidgets in myDict.items(): - dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename]) - for key in dictWidgets: - self.widgets[key] = dictWidgets[key] - packAttrs = {"anchor":E} - if self.widgets[key].type == "checkbox": - packAttrs["anchor"] = W - self.widgets[key].pack(packAttrs) - - ## Enemizer CLI Path - enemizerPathFrame = Frame(self.frames["bottomEnemizerFrame"]) - enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ") - enemizerCLIlabel.pack(side=LEFT) - enemizerURL = Label(enemizerPathFrame, text="(get online)", fg="blue", cursor="hand2") - enemizerURL.pack(side=LEFT) - enemizerURL.bind("", open_enemizer_download) - self.enemizerCLIpathVar = StringVar(value=settings["enemizercli"]) - enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=self.enemizerCLIpathVar) - enemizerCLIpathEntry.pack(side=LEFT, fill=X, expand=True) - def EnemizerSelectPath(): - path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join(".")) - if path: - self.enemizerCLIpathVar.set(path) - settings["enemizercli"] = path - enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath) - enemizerCLIbrowseButton.pack(side=LEFT) - enemizerPathFrame.pack(fill=X) - - return self,settings diff --git a/gui/randomize/generation.py b/gui/randomize/generation.py deleted file mode 100644 index d5f5ce1c..00000000 --- a/gui/randomize/generation.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -from tkinter import ttk, filedialog, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, E, W, LEFT, RIGHT, X -import gui.widgets as widgets -import json -import os - -def generation_page(parent,settings): - # Generation Setup - self = ttk.Frame(parent) - - # Generation Setup options - self.widgets = {} - - # Generation Setup option sections - self.frames = {} - self.frames["checkboxes"] = Frame(self) - self.frames["checkboxes"].pack(anchor=W) - - with open(os.path.join("resources","app","gui","randomize","generation","checkboxes.json")) as checkboxes: - myDict = json.load(checkboxes) - dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["checkboxes"]) - for key in dictWidgets: - self.widgets[key] = dictWidgets[key] - self.widgets[key].pack(anchor=W) - - self.frames["baserom"] = Frame(self) - self.frames["baserom"].pack(anchor=W, fill=X) - ## Locate base ROM - baseRomFrame = Frame(self.frames["baserom"]) - baseRomLabel = Label(baseRomFrame, text='Base Rom: ') - self.romVar = StringVar() - romEntry = Entry(baseRomFrame, textvariable=self.romVar) - self.romVar.set(settings["rom"]) - - def RomSelect(): - rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")], initialdir=os.path.join(".")) - self.romVar.set(rom) - romSelectButton = Button(baseRomFrame, text='Select Rom', command=RomSelect) - - baseRomLabel.pack(side=LEFT) - romEntry.pack(side=LEFT, fill=X, expand=True) - romSelectButton.pack(side=LEFT) - baseRomFrame.pack(fill=X) - - return self,settings diff --git a/gui/randomize/multiworld.py b/gui/randomize/multiworld.py deleted file mode 100644 index 646b02d1..00000000 --- a/gui/randomize/multiworld.py +++ /dev/null @@ -1,38 +0,0 @@ -from tkinter import ttk, StringVar, Entry, Frame, Label, Spinbox, N, E, W, X, LEFT, RIGHT -import gui.widgets as widgets -import json -import os - -def multiworld_page(parent,settings): - # Multiworld - self = ttk.Frame(parent) - - # Multiworld options - self.widgets = {} - - # Multiworld option sections - self.frames = {} - self.frames["widgets"] = Frame(self) - self.frames["widgets"].pack(anchor=W, fill=X) - - with open(os.path.join("resources","app","gui","randomize","multiworld","widgets.json")) as multiworldItems: - myDict = json.load(multiworldItems) - dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"]) - for key in dictWidgets: - self.widgets[key] = dictWidgets[key] - self.widgets[key].pack(side=LEFT, anchor=N) - - ## List of Player Names - key = "names" - self.widgets[key] = Frame(self.frames["widgets"]) - self.widgets[key].label = Label(self.widgets[key], text='Player names') - self.widgets[key].storageVar = StringVar(value=settings["names"]) - def saveMultiNames(caller,_,mode): - settings["names"] = self.widgets[key].storageVar.get() - self.widgets[key].storageVar.trace_add("write",saveMultiNames) - self.widgets[key].textbox = Entry(self.widgets[key], textvariable=self.widgets[key].storageVar) - self.widgets[key].label.pack(side=LEFT) - self.widgets[key].textbox.pack(side=LEFT, fill=X, expand=True) - self.widgets[key].pack(anchor=N, fill=X, expand=True) - - return self,settings diff --git a/gui/startinventory/__init__.py b/gui/startinventory/__init__.py deleted file mode 100644 index fa319bae..00000000 --- a/gui/startinventory/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# do nothing, just exist to make "gui.startinventory" package diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 00000000..a1a6b28e --- /dev/null +++ b/resources/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "resources" package diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json new file mode 100644 index 00000000..cc639ab5 --- /dev/null +++ b/resources/app/cli/args.json @@ -0,0 +1,323 @@ +{ + "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": [ + "default", + "off", + "on", + "pickup" + ] + }, + "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" + }, + "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": {}, + "create_rom": { + "action": "store_false", + "type": "bool" + }, + "suppress_rom": { + "action": "store_true", + "dest": "create_rom", + "help": "suppress" + }, + "shuffleganon": { + "action": "store_false", + "type": "bool" + }, + "no_shuffleganon": { + "action": "store_true", + "dest": "shuffleganon", + "help": "suppress" + }, + "calc_playthrough": { + "action": "store_false", + "type": "bool" + }, + "skip_playthrough": { + "action": "store_true", + "dest": "calc_playthrough", + "help": "suppress" + }, + "gui": { + "action": "store_true" + }, + "jsonout": { + "action": "store_true" + }, + "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..6edf5496 --- /dev/null +++ b/resources/app/cli/lang/en.json @@ -0,0 +1,278 @@ +{ + "cli": { + "yes": "Yes", + "no": "No", + "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", + "patching.spoiler": "Creating Spoiler", + "calc.playthrough": "Calculating Playthrough", + "made.rom": "Patched ROM: %s", + "made.playthrough": "Printed Playthrough: %s", + "made.spoiler": "Printed Spoiler: %s", + "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)" ], + "shuffleganon": [ + "Include the Ganon's Tower and Pyramid Hole in the", + "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." + ], + "create_rom": [ "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/adjust/overview/widgets.json b/resources/app/gui/adjust/overview/widgets.json index 30fe2445..f56bfd08 100644 --- a/resources/app/gui/adjust/overview/widgets.json +++ b/resources/app/gui/adjust/overview/widgets.json @@ -1,32 +1,11 @@ { "checkboxes": { - "nobgm": { - "type": "checkbox", - "label": { - "text": "Disable Music & MSU-1" - } - }, - "quickswap": { - "type": "checkbox", - "label": { - "text": "L/R Quickswapping" - } - } + "nobgm": { "type": "checkbox" }, + "quickswap": { "type": "checkbox" } }, "leftAdjustFrame": { "heartcolor": { "type": "selectbox", - "label": { - "text": "Heart Color" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, "options": { "Red": "red", "Blue": "blue", @@ -37,18 +16,7 @@ }, "heartbeep": { "type": "selectbox", - "label": { - "text": "Heart Beep sound rate" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Normal" - }, + "default": "Normal", "options": { "Double": "double", "Normal": "normal", @@ -61,18 +29,7 @@ "rightAdjustFrame": { "menuspeed": { "type": "selectbox", - "label": { - "text": "Menu Speed" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Normal" - }, + "default": "Normal", "options": { "Instant": "instant", "Quadruple": "quadruple", @@ -84,17 +41,6 @@ }, "owpalettes": { "type": "selectbox", - "label": { - "text": "Overworld Palettes" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, "options": { "Default": "default", "Random": "random", @@ -103,17 +49,6 @@ }, "uwpalettes": { "type": "selectbox", - "label": { - "text": "Underworld Palettes" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, "options": { "Default": "default", "Random": "random", diff --git a/resources/app/gui/custom/overview/widgets.json b/resources/app/gui/custom/overview/widgets.json index faaa9637..ee7be421 100644 --- a/resources/app/gui/custom/overview/widgets.json +++ b/resources/app/gui/custom/overview/widgets.json @@ -6,12 +6,7 @@ "text": "Bow" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "progressivebow": { "type": "textbox", @@ -19,12 +14,7 @@ "text": "Progressive Bow" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 2 - } + "default": 2 }, "boomerang": { "type": "textbox", @@ -32,12 +22,7 @@ "text": "Blue Boomerang" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "redmerang": { "type": "textbox", @@ -45,12 +30,7 @@ "text": "Red Boomerang" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "hookshot": { "type": "textbox", @@ -58,12 +38,7 @@ "text": "Hookshot" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "mushroom": { "type": "textbox", @@ -71,12 +46,7 @@ "text": "Mushroom" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "powder": { "type": "textbox", @@ -84,12 +54,7 @@ "text": "Magic Powder" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "firerod": { "type": "textbox", @@ -97,12 +62,7 @@ "text": "Fire Rod" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "icerod": { "type": "textbox", @@ -110,12 +70,7 @@ "text": "Ice Rod" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "bombos": { "type": "textbox", @@ -123,12 +78,7 @@ "text": "Bombos" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "ether": { "type": "textbox", @@ -136,12 +86,7 @@ "text": "Ether" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "quake": { "type": "textbox", @@ -149,12 +94,7 @@ "text": "Quake" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "lamp": { "type": "textbox", @@ -162,12 +102,7 @@ "text": "Lamp" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "hammer": { "type": "textbox", @@ -175,12 +110,7 @@ "text": "Hammer" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "shovel": { "type": "textbox", @@ -188,12 +118,7 @@ "text": "Shovel" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 } }, "itemList2": { @@ -203,12 +128,7 @@ "text": "Flute" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "bugnet": { "type": "textbox", @@ -216,12 +136,7 @@ "text": "Bug Net" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "book": { "type": "textbox", @@ -229,12 +144,7 @@ "text": "Book" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "bottle": { "type": "textbox", @@ -242,12 +152,7 @@ "text": "Bottle" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 4 - } + "default": 4 }, "somaria": { "type": "textbox", @@ -255,12 +160,7 @@ "text": "Cane of Somaria" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "byrna": { "type": "textbox", @@ -268,12 +168,7 @@ "text": "Cane of Byrna" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "cape": { "type": "textbox", @@ -281,12 +176,7 @@ "text": "Magic Cape" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "mirror": { "type": "textbox", @@ -294,12 +184,7 @@ "text": "Magic Mirror" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "boots": { "type": "textbox", @@ -307,12 +192,7 @@ "text": "Pegasus Boots" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "powerglove": { "type": "textbox", @@ -320,12 +200,7 @@ "text": "Power Glove" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "titansmitt": { "type": "textbox", @@ -333,12 +208,7 @@ "text": "Titan's Mitt" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "progressiveglove": { "type": "textbox", @@ -346,12 +216,7 @@ "text": "Progressive Glove" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 2 - } + "default": 2 }, "flippers": { "type": "textbox", @@ -359,12 +224,7 @@ "text": "Flippers" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "pearl": { "type": "textbox", @@ -372,12 +232,7 @@ "text": "Moon Pearl" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "heartpiece": { "type": "textbox", @@ -385,12 +240,7 @@ "text": "Piece of Heart" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 24 - } + "default": 24 } }, "itemList3": { @@ -400,12 +250,7 @@ "text": "Heart Container" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 10 - } + "default": 10 }, "sancheart": { "type": "textbox", @@ -413,12 +258,7 @@ "text": "Sanctuary Heart" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "sword1": { "type": "textbox", @@ -426,12 +266,7 @@ "text": "Fighters' Sword" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "sword2": { "type": "textbox", @@ -439,12 +274,7 @@ "text": "Master Sword" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "sword3": { "type": "textbox", @@ -452,12 +282,7 @@ "text": "Tempered Sword" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "sword4": { "type": "textbox", @@ -465,12 +290,7 @@ "text": "Golden Sword" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "progressivesword": { "type": "textbox", @@ -478,12 +298,7 @@ "text": "Progressive Sword" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 4 - } + "default": 4 }, "shield1": { "type": "textbox", @@ -491,12 +306,7 @@ "text": "Fighters' Shield" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "shield2": { "type": "textbox", @@ -504,12 +314,7 @@ "text": "Fire Shield" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "shield3": { "type": "textbox", @@ -517,12 +322,7 @@ "text": "Mirror Shield" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "progressiveshield": { "type": "textbox", @@ -530,12 +330,7 @@ "text": "Progressive Shield" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 3 - } + "default": 3 }, "mail2": { "type": "textbox", @@ -543,12 +338,7 @@ "text": "Blue Mail" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "mail3": { "type": "textbox", @@ -556,12 +346,7 @@ "text": "Red Mail" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "progressivemail": { "type": "textbox", @@ -569,12 +354,7 @@ "text": "Progressive Mail" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 2 - } + "default": 2 }, "halfmagic": { "type": "textbox", @@ -582,12 +362,7 @@ "text": "Half Magic" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 } }, "itemList4": { @@ -597,12 +372,7 @@ "text": "Quarter Magic" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "bombsplus5": { "type": "textbox", @@ -610,12 +380,7 @@ "text": "Bomb Cap +5" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "bombsplus10": { "type": "textbox", @@ -623,12 +388,7 @@ "text": "Bomb Cap +10" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "arrowsplus5": { "type": "textbox", @@ -636,12 +396,7 @@ "text": "Arrow Cap +5" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "arrowsplus10": { "type": "textbox", @@ -649,12 +404,7 @@ "text": "Arrow Cap +10" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "arrow1": { "type": "textbox", @@ -662,12 +412,7 @@ "text": "Arrow (1)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "arrow10": { "type": "textbox", @@ -675,12 +420,7 @@ "text": "Arrow (10)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 12 - } + "default": 12 }, "bomb1": { "type": "textbox", @@ -688,12 +428,7 @@ "text": "Bomb (1)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "bomb3": { "type": "textbox", @@ -701,12 +436,7 @@ "text": "Bomb (3)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 16 - } + "default": 16 }, "bomb10": { "type": "textbox", @@ -714,12 +444,7 @@ "text": "Bomb (10)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 }, "rupee1": { "type": "textbox", @@ -727,12 +452,7 @@ "text": "Rupee (1)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 2 - } + "default": 2 }, "rupee5": { "type": "textbox", @@ -740,12 +460,7 @@ "text": "Rupee (5)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 4 - } + "default": 4 }, "rupee20": { "type": "textbox", @@ -753,12 +468,7 @@ "text": "Rupee (20)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 28 - } + "default": 28 }, "rupee50": { "type": "textbox", @@ -766,12 +476,7 @@ "text": "Rupee (50)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 7 - } + "default": 7 }, "rupee100": { "type": "textbox", @@ -779,12 +484,7 @@ "text": "Rupee (100)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 1 - } + "default": 1 } }, "itemList5": { @@ -794,12 +494,7 @@ "text": "Rupee (300)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 5 - } + "default": 5 }, "blueclock": { "type": "textbox", @@ -807,12 +502,7 @@ "text": "Blue Clock" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "greenclock": { "type": "textbox", @@ -820,12 +510,7 @@ "text": "Green Clock" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "redclock": { "type": "textbox", @@ -833,12 +518,7 @@ "text": "Red Clock" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "silversupgrade": { "type": "textbox", @@ -846,12 +526,7 @@ "text": "Silver Arrows Upgrade" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "generickeys": { "type": "textbox", @@ -859,12 +534,7 @@ "text": "Generic Keys" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "triforcepieces": { "type": "textbox", @@ -872,12 +542,7 @@ "text": "Triforce Pieces" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "triforcepiecesgoal": { "type": "textbox", @@ -885,12 +550,7 @@ "text": "Triforce Pieces Goal" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "triforce": { "type": "textbox", @@ -898,12 +558,7 @@ "text": "Triforce (win game)" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "rupoor": { "type": "textbox", @@ -911,12 +566,7 @@ "text": "Rupoor" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 0 - } + "default": 0 }, "rupoorcost": { "type": "textbox", @@ -924,12 +574,7 @@ "text": "Rupoor Cost" }, "manager": "grid", - "managerAttrs": { - "label": { - "sticky": "w" - }, - "default": 10 - } + "default": 10 } } -} \ No newline at end of file +} diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json new file mode 100644 index 00000000..da499b03 --- /dev/null +++ b/resources/app/gui/lang/en.json @@ -0,0 +1,271 @@ +{ + "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.createrom": "Create Patched ROM", + "randomizer.generation.calcplaythrough": "Calculate Playthrough", + "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", + + + "bottom.content.worlds": "Worlds", + "bottom.content.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/keysanity.json b/resources/app/gui/randomize/dungeon/keysanity.json index 1be4c3fd..49a17237 100644 --- a/resources/app/gui/randomize/dungeon/keysanity.json +++ b/resources/app/gui/randomize/dungeon/keysanity.json @@ -1,26 +1,8 @@ { - "mapshuffle": { - "type": "checkbox", - "label": { - "text": "Maps" - } - }, - "compassshuffle": { - "type": "checkbox", - "label": { - "text": "Compasses" - } - }, - "smallkeyshuffle": { - "type": "checkbox", - "label": { - "text": "Small Keys" - } - }, - "bigkeyshuffle": { - "type": "checkbox", - "label": { - "text": "Big Keys" - } + "keysanity": { + "mapshuffle": { "type": "checkbox" }, + "compassshuffle": { "type": "checkbox" }, + "smallkeyshuffle": { "type": "checkbox" }, + "bigkeyshuffle": { "type": "checkbox" } } } diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 295f3098..04082822 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -1,49 +1,24 @@ { - "dungeondoorshuffle": { - "type": "selectbox", - "label": { - "text": "Dungeon Door Shuffle" + "widgets": { + "dungeondoorshuffle": { + "type": "selectbox", + "default": "basic", + "options": [ + "vanilla", + "basic", + "crossed" + ] }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Basic" - }, - "options": { - "Vanilla": "vanilla", - "Basic": "basic", - "Crossed": "crossed" - } - }, - "experimental": { - "type": "checkbox", - "label": { - "text": "Enable Experimental Features" - } - }, - "dungeon_counters": { - "type": "selectbox", - "label": { - "text": "Dungeon Chest Counters" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Auto" - }, - "options": { - "Auto": "default", - "Off": "off", - "On": "on", - "On Compass Pickup": "pickup" + "experimental": { "type": "checkbox" }, + "dungeon_counters": { + "type": "selectbox", + "default": "default", + "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..8103e8cb 100644 --- a/resources/app/gui/randomize/enemizer/widgets.json +++ b/resources/app/gui/randomize/enemizer/widgets.json @@ -1,93 +1,44 @@ { "checkboxes": { - "potshuffle": { - "type": "checkbox", - "label": { - "text": "Pot Shuffle" - } - } + "potshuffle": { "type": "checkbox" } }, "leftEnemizerFrame": { "enemyshuffle": { "type": "selectbox", - "label": { - "text": "Enemy Shuffle" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Vanilla": "none", - "Shuffled": "shuffled", - "Chaos": "chaos" - } + "options": [ + "none", + "shuffled", + "chaos" + ] }, "bossshuffle": { "type": "selectbox", - "label": { - "text": "Boss Shuffle" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Vanilla": "none", - "Basic": "basic", - "Shuffled": "shuffled", - "Chaos": "chaos" - } + "options": [ + "none", + "basic", + "shuffled", + "chaos" + ] } }, "rightEnemizerFrame": { "enemydamage": { "type": "selectbox", - "label": { - "text": "Enemy Damage" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Vanilla": "default", - "Shuffled": "shuffled", - "Chaos": "chaos" - } + "options": [ + "default", + "shuffled", + "chaos" + ] }, "enemyhealth": { "type": "selectbox", - "label": { - "text": "Enemy Health" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Vanilla": "default", - "Easy": "easy", - "Normal": "normal", - "Hard": "hard", - "Expert": "expert" - } + "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..e46ee36e 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -1,40 +1,23 @@ { "widgets": { - "openpyramid": { - "type": "checkbox", - "label": { - "text": "Pre-open Pyramid Hole" - } - }, - "shuffleganon": { - "type": "checkbox", - "label": { - "text": "Include Ganon's Tower and Pyramid Hole in shuffle pool" - } - }, - "entranceshuffle": { + "openpyramid": { "type": "checkbox" }, + "shuffleganon": { "type": "checkbox" }, + "entranceshuffle": { "type": "selectbox", - "label": { - "text": "Entrance Shuffle" - }, - "managerAttrs": { - "label": { "side": "left" }, - "selectbox": { "side": "right" } - }, - "options": { - "Vanilla": "vanilla", - "Simple": "simple", - "Restricted": "restricted", - "Full": "full", - "Crossed": "crossed", - "Insanity": "insanity", - "Restricted (Legacy)": "restricted_legacy", - "Full (Legacy)": "full_legacy", - "Madness (Legacy)": "madness_legacy", - "Insanity (Legacy)": "insanity_legacy", - "Dungeons + Full": "dungeonsfull", - "Dungeons + Simple": "dungeonssimple" - } + "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..815f72b1 100644 --- a/resources/app/gui/randomize/gameoptions/widgets.json +++ b/resources/app/gui/randomize/gameoptions/widgets.json @@ -1,131 +1,63 @@ { - "checkboxes": { + "checkboxes": { "hints": { "type": "checkbox", - "label": { - "text": "Include Helpful Hints" - }, "default": "true" }, - "nobgm": { - "type": "checkbox", - "label": { - "text": "Disable Music & MSU-1" - } - }, - "quickswap": { - "type": "checkbox", - "label": { - "text": "L/R Quickswapping" - } - } + "nobgm": { "type": "checkbox" }, + "quickswap": { "type": "checkbox" } }, "leftRomOptionsFrame": { "heartcolor": { "type": "selectbox", - "label": { - "text": "Heart Color" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Red": "red", - "Blue": "blue", - "Green": "green", - "Yellow": "yellow", - "Random": "random" - } + "options": [ + "red", + "blue", + "green", + "yellow", + "random" + ] }, "heartbeep": { "type": "selectbox", - "label": { - "text": "Heart Beep sound rate" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Normal" - }, - "options": { - "Double": "double", - "Normal": "normal", - "Half": "half", - "Quarter": "quarter", - "Off": "off" - } + "default": "normal", + "options": [ + "double", + "normal", + "half", + "quarter", + "off" + ] } }, "rightRomOptionsFrame": { "menuspeed": { "type": "selectbox", - "label": { - "text": "Menu Speed" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Normal" - }, - "options": { - "Instant": "instant", - "Quadruple": "quadruple", - "Triple": "triple", - "Double": "double", - "Normal": "normal", - "Half": "half" - } + "default": "normal", + "options": [ + "instant", + "quadruple", + "triple", + "double", + "normal", + "half" + ] }, "owpalettes": { "type": "selectbox", - "label": { - "text": "Overworld Palettes" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Default": "default", - "Random": "random", - "Blackout": "blackout" - } + "options": [ + "default", + "random", + "blackout" + ] }, "uwpalettes": { "type": "selectbox", - "label": { - "text": "Underworld Palettes" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "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..db020e6d 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -1,45 +1,9 @@ { - "spoiler": { - "type": "checkbox", - "label": { - "text": "Create Spoiler Log" - } - }, - "suppressrom": { - "type": "checkbox", - "label": { - "text": "Do not create patched ROM" - } - }, - "usestartinventory": { - "type": "checkbox", - "label": { - "text": "Use starting inventory" - } - }, - "usecustompool": { - "type": "checkbox", - "label": { - "text": "Use custom item pool" - } - }, - "saveonexit": { - "type": "selectbox", - "label": { - "text": "Save Settings on Exit" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Ask Me": "ask", - "Always": "always", - "Never": "never" - } + "checkboxes": { + "spoiler": { "type": "checkbox" }, + "createrom": { "type": "checkbox" }, + "calcplaythrough": { "type": "checkbox" }, + "usestartinventory": { "type": "checkbox" }, + "usecustompool": { "type": "checkbox" } } } diff --git a/resources/app/gui/randomize/generation/widgets.json b/resources/app/gui/randomize/generation/widgets.json new file mode 100644 index 00000000..22f9decc --- /dev/null +++ b/resources/app/gui/randomize/generation/widgets.json @@ -0,0 +1,12 @@ +{ + "widgets": { + "saveonexit": { + "type": "selectbox", + "options": [ + "ask", + "always", + "never" + ] + } + } +} diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 0c427418..d485cdb8 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -1,266 +1,116 @@ { "checkboxes": { - "retro": { - "type": "checkbox", - "label": { - "text": "Retro mode (universal keys)" - } - } + "retro": { "type": "checkbox" } }, "leftItemFrame": { "worldstate": { "type": "selectbox", - "label": { - "text": "World State" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Open" - }, - "options": { - "Standard": "standard", - "Open": "open", - "Inverted": "inverted" - } + "default": "open", + "options": [ + "standard", + "open", + "inverted", + "retro" + ] }, "logiclevel": { "type": "selectbox", - "label": { - "text": "Logic Level" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "No Glitches": "noglitches", - "Minor Glitches": "minorglitches", - "No Logic": "nologic" - } + "options": [ + "noglitches", + "minorglitches", + "nologic" + ] }, "goal": { "type": "selectbox", - "label": { - "text": "Goal" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Defeat Ganon": "ganon", - "Master Sword Pedestal": "pedestal", - "All Dungeons": "dungeons", - "Triforce Hunt": "triforcehunt", - "Crystals": "crystals" - } + "options": [ + "ganon", + "pedestal", + "dungeons", + "triforcehunt", + "crystals" + ] }, "crystals_gt": { "type": "selectbox", - "label": { - "text": "Crystals to open GT" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "0": "0", - "1": "1", - "2": "2", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "Random": "random" - } + "options": [ + 0, 1, 2, 3, 4, 5, 6, 7, + "random" + ] }, "crystals_ganon": { "type": "selectbox", - "label": { - "text": "Crystals to harm Ganon" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "0": "0", - "1": "1", - "2": "2", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "Random": "random" - } + "options": [ + 0, 1, 2, 3, 4, 5, 6, 7, + "random" + ] }, "weapons": { "type": "selectbox", - "label": { - "text": "Weapons" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Randomized": "random", - "Assured": "assured", - "Swordless": "swordless", - "Vanilla": "vanilla" - } + "options": [ + "random", + "assured", + "swordless", + "vanilla" + ] } }, "rightItemFrame": { "itempool": { "type": "selectbox", - "label": { - "text": "Item Pool" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Normal": "normal", - "Hard": "hard", - "Expert": "expert" - } + "options": [ + "normal", + "hard", + "expert" + ] }, "itemfunction": { "type": "selectbox", - "label": { - "text": "Item Functionality" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "Normal": "normal", - "Hard": "hard", - "Expert": "expert" - } + "options": [ + "normal", + "hard", + "expert" + ] }, "timer": { "type": "selectbox", - "label": { - "text": "Timer Setting" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "No Timer": "none", - "Stopwatch": "display", - "Timed": "timed", - "Timed OHKO": "timed-ohko", - "OHKO": "ohko", - "Timed Countdown": "timed-countdown" - } + "options": [ + "none", + "display", + "timed", + "timed-ohko", + "ohko", + "timed-countdown" + ] }, "progressives": { "type": "selectbox", - "label": { - "text": "Progressive Items" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "On": "on", - "Off": "off", - "Random": "random" - } + "options": [ + "on", + "off", + "random" + ] }, "accessibility": { "type": "selectbox", - "label": { - "text": "Accessibility" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - } - }, - "options": { - "100% Inventory": "items", - "100% Locations": "locations", - "Beatable": "none" - } + "options": [ + "items", + "locations", + "none" + ] }, "sortingalgo": { "type": "selectbox", - "label": { - "text": "Item Sorting" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "selectbox": { - "side": "right" - }, - "default": "Balanced" - }, - "options": { - "Freshness": "freshness", - "Flood": "flood", - "VT8.21": "vt21", - "VT8.22": "vt22", - "VT8.25": "vt25", - "VT8.26": "vt26", - "Balanced": "balanced" - } + "default": "balanced", + "options": [ + "freshness", + "flood", + "vt21", + "vt22", + "vt25", + "vt26", + "balanced" + ] } } } diff --git a/resources/app/gui/randomize/multiworld/widgets.json b/resources/app/gui/randomize/multiworld/widgets.json index b943c017..327b9434 100644 --- a/resources/app/gui/randomize/multiworld/widgets.json +++ b/resources/app/gui/randomize/multiworld/widgets.json @@ -1,16 +1,5 @@ { - "worlds": { - "type": "spinbox", - "label": { - "text": "Worlds" - }, - "managerAttrs": { - "label": { - "side": "left" - }, - "spinbox": { - "side": "right" - } - } + "widgets": { + "worlds": { "type": "spinbox" } } } 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/resources/app/meta/manifests/pip_requirements.txt b/resources/app/meta/manifests/pip_requirements.txt new file mode 100644 index 00000000..93e06fa8 --- /dev/null +++ b/resources/app/meta/manifests/pip_requirements.txt @@ -0,0 +1 @@ +aenum diff --git a/resources/ci/__init__.py b/resources/ci/__init__.py new file mode 100644 index 00000000..04deec10 --- /dev/null +++ b/resources/ci/__init__.py @@ -0,0 +1 @@ +#do nothing, just exist to make "resources.ci" package diff --git a/resources/ci/common/__init__.py b/resources/ci/common/__init__.py new file mode 100644 index 00000000..34761450 --- /dev/null +++ b/resources/ci/common/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "resources.ci.common" package diff --git a/resources/ci/common/common.py b/resources/ci/common/common.py new file mode 100644 index 00000000..e8b871a7 --- /dev/null +++ b/resources/ci/common/common.py @@ -0,0 +1,137 @@ +import os # for env vars +import stat # file statistics + +# take number of bytes and convert to string with units measure +def convert_bytes(num): + for x in ["bytes","KB","MB","GB","TB","PB"]: + if num < 1024.0: + return "%3.1f %s" % (num,x) + num /= 1024.0 + +# get filesize of file at path +def file_size(file_path): + if os.path.isfile(file_path): + file_info = os.stat(file_path) + return convert_bytes(file_info.st_size) + +# prepare environment variables +def prepare_env(): + DEFAULT_EVENT = "event" + DEFAULT_REPO_SLUG = "miketrethewey/ALttPDoorRandomizer" + + env = {} + + # get app version + APP_VERSION = "" + APP_VERSION_FILE = "./resources/app/meta/manifests/app_version.txt" + if os.path.isfile(APP_VERSION_FILE): + with open(APP_VERSION_FILE,"r") as f: + APP_VERSION = f.readlines()[0].strip() + # ci data + env["CI_SYSTEM"] = os.getenv("CI_SYSTEM","") + # git data + env["BRANCH"] = os.getenv("TRAVIS_BRANCH","") + env["GITHUB_ACTOR"] = os.getenv("GITHUB_ACTOR","MegaMan.EXE") + env["GITHUB_SHA"] = os.getenv("GITHUB_SHA","") + env["GITHUB_RUN_ID"] = os.getenv("GITHUB_RUN_ID","") + env["GITHUB_SHA_SHORT"] = env["GITHUB_SHA"] + # commit data + env["COMMIT_ID"] = os.getenv("TRAVIS_COMMIT",os.getenv("GITHUB_SHA","")) + env["COMMIT_COMPARE"] = os.getenv("TRAVIS_COMMIT_RANGE","") + # event data + env["EVENT_MESSAGE"] = os.getenv("TRAVIS_COMMIT_MESSAGE","") + env["EVENT_LOG"] = os.getenv("GITHUB_EVENT_PATH","") + env["EVENT_TYPE"] = os.getenv("TRAVIS_EVENT_TYPE",os.getenv("GITHUB_EVENT_NAME",DEFAULT_EVENT)) + # repo data + env["REPO_SLUG"] = os.getenv("TRAVIS_REPO_SLUG",os.getenv("GITHUB_REPOSITORY",DEFAULT_REPO_SLUG)) + env["REPO_USERNAME"] = "" + env["REPO_NAME"] = "" + + # repo slug + if '/' in env["REPO_SLUG"]: + tmp = env["REPO_SLUG"].split('/') + env["REPO_USERNAME"] = tmp[0] + env["REPO_NAME"] = tmp[1] + + if not env["GITHUB_SHA"] == "": + env["GITHUB_SHA_SHORT"] = env["GITHUB_SHA"][:7] + + # ci data + env["BUILD_NUMBER"] = os.getenv("TRAVIS_BUILD_NUMBER",env["GITHUB_RUN_ID"]) + + GITHUB_TAG = os.getenv("TRAVIS_TAG",os.getenv("GITHUB_TAG","")) + OS_NAME = os.getenv("TRAVIS_OS_NAME",os.getenv("OS_NAME","")).replace("macOS","osx") + OS_DIST = os.getenv("TRAVIS_DIST","notset") + OS_VERSION = "" + + if '-' in OS_NAME: + OS_VERSION = OS_NAME[OS_NAME.find('-')+1:] + OS_NAME = OS_NAME[:OS_NAME.find('-')] + if OS_NAME == "linux" or OS_NAME == "ubuntu": + if OS_VERSION == "latest": + OS_VERSION = "bionic" + elif OS_VERSION == "16.04": + OS_VERSION = "xenial" + OS_DIST = OS_VERSION + + if OS_VERSION == "" and not OS_DIST == "" and not OS_DIST == "notset": + OS_VERSION = OS_DIST + + # if no tag + if GITHUB_TAG == "": + # if we haven't appended the build number, do it + if env["BUILD_NUMBER"] not in GITHUB_TAG: + GITHUB_TAG = APP_VERSION + # if the app version didn't have the build number, add it + # set to . + if env["BUILD_NUMBER"] not in GITHUB_TAG: + GITHUB_TAG += '.' + env["BUILD_NUMBER"] + + env["GITHUB_TAG"] = GITHUB_TAG + env["OS_NAME"] = OS_NAME + env["OS_DIST"] = OS_DIST + env["OS_VERSION"] = OS_VERSION + + return env + +# build filename based on metadata +def prepare_filename(BUILD_FILENAME): + env = prepare_env() + + DEST_FILENAME = "" + + # build the filename + if not BUILD_FILENAME == "": + os.chmod(BUILD_FILENAME,0o755) + fileparts = os.path.splitext(BUILD_FILENAME) + DEST_SLUG = fileparts[0] + DEST_EXTENSION = fileparts[1] + DEST_SLUG = DEST_SLUG + '-' + env["GITHUB_TAG"] + '-' + env["OS_NAME"] + if not env["OS_DIST"] == "" and not env["OS_DIST"] == "notset": + DEST_SLUG += '-' + env["OS_DIST"] + DEST_FILENAME = DEST_SLUG + DEST_EXTENSION + return DEST_FILENAME + +# find a binary file if it's executable +# failing that, assume it's over 6MB +def find_binary(listdir): + FILENAME_CHECKS = [ "Gui", "DungeonRandomizer" ] + FILESIZE_CHECK = (6 * 1024 * 1024) # 6MB + + BUILD_FILENAMES = [] + executable = stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH + for filename in os.listdir(listdir): + if os.path.isfile(filename): + if os.path.splitext(filename)[1] != ".py": + st = os.stat(filename) + mode = st.st_mode + big = st.st_size > FILESIZE_CHECK + if (mode & executable) or big: + for check in FILENAME_CHECKS: + if check in filename: + BUILD_FILENAMES.append(filename) + return BUILD_FILENAMES + +if __name__ == "__main__": + env = prepare_env() + print(env) diff --git a/resources/ci/common/get_upx.py b/resources/ci/common/get_upx.py new file mode 100644 index 00000000..8d71098e --- /dev/null +++ b/resources/ci/common/get_upx.py @@ -0,0 +1,42 @@ +import common +import os # for env vars +import sys # for path +import urllib.request # for downloads +from shutil import unpack_archive + +# only do stuff if we don't have a UPX folder + +if not os.path.isdir("./upx"): + # get env vars + env = common.prepare_env() + # set up download url + UPX_VERSION = os.getenv("UPX_VERSION") or "3.96" + UPX_SLUG = "" + UPX_FILE = "" + if "windows" in env["OS_NAME"]: + UPX_SLUG = "upx-" + UPX_VERSION + "-win64" + UPX_FILE = UPX_SLUG + ".zip" + else: + UPX_SLUG = "upx-" + UPX_VERSION + "-amd64_linux" + UPX_FILE = UPX_SLUG + ".tar.xz" + UPX_URL = "https://github.com/upx/upx/releases/download/v" + UPX_VERSION + '/' + UPX_FILE + + if "osx" not in env["OS_NAME"]: + + print("Getting UPX: " + UPX_FILE) + + with open("./" + UPX_FILE,"wb") as upx: + UPX_REQ = urllib.request.Request( + UPX_URL, + data=None + ) + UPX_REQ = urllib.request.urlopen(UPX_REQ) + UPX_DATA = UPX_REQ.read() + upx.write(UPX_DATA) + + unpack_archive(UPX_FILE,"./") + + os.rename("./" + UPX_SLUG,"./upx") + os.remove("./" + UPX_FILE) + +print("UPX should " + ("not " if not os.path.isdir("./upx") else "") + "be available.") diff --git a/resources/ci/common/git_clean.py b/resources/ci/common/git_clean.py new file mode 100644 index 00000000..7c2e2cfa --- /dev/null +++ b/resources/ci/common/git_clean.py @@ -0,0 +1,20 @@ +import subprocess # do stuff at the shell level + +def git_clean(): + excludes = [ + ".vscode", # vscode IDE files + ".idea", # idea IDE files + "*.json", # keep JSON files for that one time I just nuked all that I was working on, oops + "*app*version.*" # keep appversion files + ] + excludes = ['--exclude={0}'.format(exclude) for exclude in excludes] + + # clean the git slate + subprocess.check_call([ + "git", # run a git command + "clean", # clean command + "-dfx", # d: directories, f: files, x: ignored files + *excludes]) + +if __name__ == "__main__": + git_clean() diff --git a/resources/ci/common/install.py b/resources/ci/common/install.py new file mode 100644 index 00000000..8cd40df2 --- /dev/null +++ b/resources/ci/common/install.py @@ -0,0 +1,27 @@ +import common +import os # for env vars +import subprocess # do stuff at the shell level + +env = common.prepare_env() + +# get executables +# python +# linux/windows: python +# macosx: python3 +# pip +# linux/macosx: pip3 +# windows: pip +PYTHON_EXECUTABLE = "python3" if "osx" in env["OS_NAME"] else "python" +PIP_EXECUTABLE = "pip" if "windows" in env["OS_NAME"] else "pip3" +PIP_EXECUTABLE = "pip" if "osx" in env["OS_NAME"] and "actions" in env["CI_SYSTEM"] else PIP_EXECUTABLE + +# upgrade pip +subprocess.check_call([PYTHON_EXECUTABLE,"-m","pip","install","--upgrade","pip"]) + +# pip version +subprocess.check_call([PIP_EXECUTABLE,"--version"]) +# if pip3, install wheel +if PIP_EXECUTABLE == "pip3": + subprocess.check_call([PIP_EXECUTABLE,"install","-U","wheel"]) +# install listed dependencies +subprocess.check_call([PIP_EXECUTABLE,"install","-r","./resources/app/meta/manifests/pip_requirements.txt"]) diff --git a/resources/ci/common/prepare_appversion.py b/resources/ci/common/prepare_appversion.py new file mode 100644 index 00000000..bd26318e --- /dev/null +++ b/resources/ci/common/prepare_appversion.py @@ -0,0 +1,20 @@ +import common +import os # for env vars +from shutil import copy # file manipulation + +env = common.prepare_env() + +# set tag to app_version.txt +if not env["GITHUB_TAG"] == "": + with open(os.path.join(".","resources","app","meta","manifests","app_version.txt"),"w+") as f: + _ = f.read() + f.seek(0) + f.write(env["GITHUB_TAG"]) + f.truncate() + +if not os.path.isdir(os.path.join("..","build")): + os.mkdir(os.path.join("..","build")) +copy( + os.path.join(".","resources","app","meta","manifests","app_version.txt"), + os.path.join("..","build","app_version.txt") +) diff --git a/resources/ci/common/prepare_binary.py b/resources/ci/common/prepare_binary.py new file mode 100644 index 00000000..583c7d7b --- /dev/null +++ b/resources/ci/common/prepare_binary.py @@ -0,0 +1,40 @@ +import distutils.dir_util # for copying trees +import os # for env vars +import stat # for file stats +import subprocess # do stuff at the shell level +import common +from shutil import copy, make_archive, move, rmtree # file manipulation + +env = common.prepare_env() + +# make dir to put the binary in +if not os.path.isdir(os.path.join("..","artifact")): + os.mkdir(os.path.join("..","artifact")) + +BUILD_FILENAME = "" + +# list executables +BUILD_FILENAME = common.find_binary('.') +if BUILD_FILENAME == "": + BUILD_FILENAME = common.find_binary(os.path.join("..","artifact")) + +if isinstance(BUILD_FILENAME,str): + BUILD_FILENAME = list(BUILD_FILENAME) + +BUILD_FILENAMES = BUILD_FILENAME + +for BUILD_FILENAME in BUILD_FILENAMES: + DEST_FILENAME = common.prepare_filename(BUILD_FILENAME) + + print("OS Name: " + env["OS_NAME"]) + print("OS Version: " + env["OS_VERSION"]) + print("Build Filename: " + BUILD_FILENAME) + print("Dest Filename: " + DEST_FILENAME) + if not BUILD_FILENAME == "": + print("Build Filesize: " + common.file_size(BUILD_FILENAME)) + + if not BUILD_FILENAME == "": + move( + os.path.join(".",BUILD_FILENAME), + os.path.join("..","artifact",BUILD_FILENAME) + ) diff --git a/resources/ci/common/prepare_release.py b/resources/ci/common/prepare_release.py new file mode 100644 index 00000000..5e32b410 --- /dev/null +++ b/resources/ci/common/prepare_release.py @@ -0,0 +1,127 @@ +import distutils.dir_util # for copying trees +import os # for env vars +import stat # for file stats +import subprocess # do stuff at the shell level +import common +from git_clean import git_clean +from shutil import copy, make_archive, move, rmtree # file manipulation + +env = common.prepare_env() # get env vars + +dirs = [ + os.path.join("..", "artifact"), # temp dir for binary + os.path.join("..", "build"), # temp dir for other stuff + os.path.join("..", "deploy") # dir for archive +] +for dirname in dirs: + if not os.path.isdir(dirname): + os.makedirs(dirname) + +# make dirs for each os +for dirname in ["linux","macos","windows"]: + if not os.path.isdir(os.path.join("..","deploy",dirname)): + os.mkdir(os.path.join("..","deploy",dirname)) + +# sanity check permissions for working_dirs.json +dirpath = "." +for dirname in ["resources","user","meta","manifests"]: + dirpath += os.path.join(dirpath,dirname) + if os.path.isdir(dirpath): + os.chmod(dirpath,0o755) + +# nuke travis file if it exists +for travis in [ os.path.join(".", ".travis.yml"), os.path.join(".", ".travis.off") ]: + if os.path.isfile(travis): + os.remove(travis) + +# nuke test suite if it exists +if os.path.isdir(os.path.join(".","tests")): + distutils.dir_util.remove_tree(os.path.join(".","tests")) + +BUILD_FILENAME = "" +ZIP_FILENAME = "" + +# list executables +BUILD_FILENAME = common.find_binary(os.path.join(".")) +if BUILD_FILENAME == "": + BUILD_FILENAME = common.find_binary(os.path.join("..","artifact")) + +if isinstance(BUILD_FILENAME,str): + BUILD_FILENAME = list(BUILD_FILENAME) + +BUILD_FILENAMES = BUILD_FILENAME + +print(BUILD_FILENAMES) + +if len(BUILD_FILENAMES) > 0: + for BUILD_FILENAME in BUILD_FILENAMES: + if not BUILD_FILENAME == "": + if not "artifact" in BUILD_FILENAME: + # move the binary to temp folder + move( + os.path.join(".",BUILD_FILENAME), + os.path.join("..","artifact",BUILD_FILENAME) + ) + + # clean the git slate + git_clean() + + # mv dirs from source code + dirs = [ + os.path.join(".",".git"), + os.path.join(".",".github"), + os.path.join(".",".gitignore"), + os.path.join(".","html"), + os.path.join(".","resources","ci") + ] + for dirname in dirs: + if os.path.isdir(dirname): + move( + dirname, + os.path.join("..", "build", dirname) + ) + + for BUILD_FILENAME in BUILD_FILENAMES: + if not "artifact" in BUILD_FILENAME: + if os.path.isfile(os.path.join("..","artifact",BUILD_FILENAME)): + # move the binary back + move( + os.path.join("..","artifact",BUILD_FILENAME), + os.path.join(".",BUILD_FILENAME) + ) + # Make Linux/Mac binary executable + if "linux" in env["OS_NAME"] or "ubuntu" in env["OS_NAME"] or "mac" in env["OS_NAME"] or "osx" in env["OS_NAME"]: + os.chmod(os.path.join(".",BUILD_FILENAME),0o755) + + # .zip if windows + # .tar.gz otherwise + ZIP_FILENAME = os.path.join("..","deploy",env["REPO_NAME"]) if len(BUILD_FILENAMES) > 1 else os.path.join("..","deploy",os.path.splitext(BUILD_FILENAME)[0]) + if env["OS_NAME"] == "windows": + make_archive(ZIP_FILENAME,"zip") + ZIP_FILENAME += ".zip" + else: + make_archive(ZIP_FILENAME,"gztar") + ZIP_FILENAME += ".tar.gz" + + # mv dirs back + for dir in dirs: + if os.path.isdir(os.path.join("..","build",dir)): + move( + os.path.join("..","build",dir), + os.path.join(".",dir) + ) + +for BUILD_FILENAME in BUILD_FILENAMES: + if not BUILD_FILENAME == "": + print("Build Filename: " + BUILD_FILENAME) + print("Build Filesize: " + common.file_size(BUILD_FILENAME)) + else: + print("No Build to prepare: " + BUILD_FILENAME) + +if not ZIP_FILENAME == "": + print("Zip Filename: " + ZIP_FILENAME) + print("Zip Filesize: " + common.file_size(ZIP_FILENAME)) +else: + print("No Zip to prepare: " + ZIP_FILENAME) + +print("Git tag: " + env["GITHUB_TAG"]) diff --git a/resources/user/.gitkeep b/resources/user/.gitkeep new file mode 100644 index 00000000..a96dac2e --- /dev/null +++ b/resources/user/.gitkeep @@ -0,0 +1 @@ +# do nothing, just exist to make "user" folder diff --git a/source/__init__.py b/source/__init__.py new file mode 100644 index 00000000..724252e9 --- /dev/null +++ b/source/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source" package diff --git a/source/classes/BabelFish.py b/source/classes/BabelFish.py new file mode 100644 index 00000000..40b6463f --- /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="", uselang=None): #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[uselang if uselang is not None else 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/classes/SpriteSelector.py b/source/classes/SpriteSelector.py similarity index 97% rename from classes/SpriteSelector.py rename to source/classes/SpriteSelector.py index 7e527d05..0defb5d6 100644 --- a/classes/SpriteSelector.py +++ b/source/classes/SpriteSelector.py @@ -1,4 +1,4 @@ -from tkinter import filedialog, messagebox, Button, Canvas, Label, LabelFrame, Frame, PhotoImage, Scrollbar, Toplevel, ALL, NSEW, LEFT, BOTTOM, X, RIGHT, TOP, HORIZONTAL, EW, NS +from tkinter import filedialog, messagebox, Button, Canvas, Label, LabelFrame, Frame, PhotoImage, Scrollbar, Toplevel, ALL, LEFT, BOTTOM, X, RIGHT, TOP, EW, NS from glob import glob import json import os @@ -34,8 +34,10 @@ class SpriteSelector(object): def open_unofficial_sprite_dir(_evt): open_file(self.unofficial_sprite_dir) + # Open SpriteSomething directory for Link sprites def open_spritesomething_listing(_evt): webbrowser.open("https://artheau.github.io/SpriteSomething/?mode=zelda3/link") +# webbrowser.open("https://artheau.github.io/SpriteSomething/resources/app/snes/zelda3/link/sprites.html") official_frametitle = Frame(self.window) official_title_text = Label(official_frametitle, text="Official Sprites") @@ -50,6 +52,7 @@ class SpriteSelector(object): unofficial_title_text.pack(side=LEFT) unofficial_title_link.pack(side=LEFT) unofficial_title_link.bind("", open_unofficial_sprite_dir) + # Include hyperlink to SpriteSomething directory for Link sprites spritesomething_title_link = Label(unofficial_frametitle, text="(SpriteSomething)", fg="blue", cursor="hand2") spritesomething_title_link.pack(side=LEFT) spritesomething_title_link.bind("", open_spritesomething_listing) diff --git a/source/classes/__init__.py b/source/classes/__init__.py new file mode 100644 index 00000000..68fbbf63 --- /dev/null +++ b/source/classes/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.classes" package diff --git a/classes/constants.py b/source/classes/constants.py similarity index 88% rename from classes/constants.py rename to source/classes/constants.py index b822628f..e8145f66 100644 --- a/classes/constants.py +++ b/source/classes/constants.py @@ -1,3 +1,4 @@ +# Ordered list of items in Custom Item Pool page and Starting Inventory page CUSTOMITEMS = [ "bow", "progressivebow", "boomerang", "redmerang", "hookshot", "mushroom", "powder", "firerod", "icerod", "bombos", @@ -20,11 +21,13 @@ CUSTOMITEMS = [ "rupoorcost" ] +# These can't be in the Starting Inventory page CANTSTARTWITH = [ "triforcepiecesgoal", "triforce", "rupoor", "rupoorcost" ] +# In the same order as CUSTOMITEMS, these are Pretty Labels for each option CUSTOMITEMLABELS = [ "Bow", "Progressive Bow", "Blue Boomerang", "Red Boomerang", "Hookshot", "Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos", @@ -33,7 +36,7 @@ CUSTOMITEMLABELS = [ "Ocarina", "Bug Catching Net", "Book of Mudora", "Bottle", "Cane of Somaria", "Cane of Byrna", "Magic Cape", "Magic Mirror", "Pegasus Boots", "Power Glove", "Titans Mitts", "Progressive Glove", "Flippers", "Moon Pearl", "Piece of Heart", - + "Boss Heart Container", "Sanctuary Heart Container", "Fighter Sword", "Master Sword", "Tempered Sword", "Golden Sword", "Progressive Sword", "Blue Shield", "Red Shield", "Mirror Shield", "Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)", @@ -47,6 +50,8 @@ CUSTOMITEMLABELS = [ "Rupoor Cost" ] +# Stuff on each page to save, according to internal names as defined by the widgets definitions +# and how it eventually translates to YAML/JSON weight files SETTINGSTOPROCESS = { "randomizer": { "item": { @@ -85,9 +90,6 @@ SETTINGSTOPROCESS = { "experimental": "experimental", "dungeon_counters": "dungeon_counters" }, - "multiworld": { - "names": "names" - }, "gameoptions": { "hints": "hints", "nobgm": "disablemusic", @@ -100,10 +102,18 @@ SETTINGSTOPROCESS = { }, "generation": { "spoiler": "create_spoiler", - "suppressrom": "suppress_rom", + "createrom": "create_rom", + "calcplaythrough": "calc_playthrough", "usestartinventory": "usestartinventory", "usecustompool": "custom", "saveonexit": "saveonexit" - } + } + }, + "bottom": { + "content": { + "names": "names", + "seed": "seed", + "generationcount": "count" + } } } diff --git a/source/gui/__init__.py b/source/gui/__init__.py new file mode 100644 index 00000000..00601e6a --- /dev/null +++ b/source/gui/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.gui" package diff --git a/source/gui/about/__init__.py b/source/gui/about/__init__.py new file mode 100644 index 00000000..5f70030c --- /dev/null +++ b/source/gui/about/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.gui.about" package diff --git a/source/gui/adjust/__init__.py b/source/gui/adjust/__init__.py new file mode 100644 index 00000000..a838ecf0 --- /dev/null +++ b/source/gui/adjust/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.gui.adjust" package diff --git a/gui/adjust/overview.py b/source/gui/adjust/overview.py similarity index 88% rename from gui/adjust/overview.py rename to source/gui/adjust/overview.py index 8e3a7851..c524d02e 100644 --- a/gui/adjust/overview.py +++ b/source/gui/adjust/overview.py @@ -1,8 +1,8 @@ -from tkinter import ttk, filedialog, messagebox, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, OptionMenu, E, W, LEFT, RIGHT, X, BOTTOM +from tkinter import ttk, filedialog, messagebox, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT, X, BOTTOM from AdjusterMain import adjust from argparse import Namespace -from classes.SpriteSelector import SpriteSelector -import gui.widgets as widgets +from source.classes.SpriteSelector import SpriteSelector +import source.gui.widgets as widgets import json import logging import os @@ -19,6 +19,7 @@ def adjust_page(top, parent, settings): self.frames["checkboxes"] = Frame(self) self.frames["checkboxes"].pack(anchor=W) + # Adjust option frames self.frames["selectOptionsFrame"] = Frame(self) self.frames["leftAdjustFrame"] = Frame(self.frames["selectOptionsFrame"]) self.frames["rightAdjustFrame"] = Frame(self.frames["selectOptionsFrame"]) @@ -28,6 +29,8 @@ def adjust_page(top, parent, settings): self.frames["rightAdjustFrame"].pack(side=RIGHT) self.frames["bottomAdjustFrame"].pack(fill=X) + # Load Adjust option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes with open(os.path.join("resources","app","gui","adjust","overview","widgets.json")) as widgetDefns: myDict = json.load(widgetDefns) for framename,theseWidgets in myDict.items(): @@ -40,6 +43,7 @@ def adjust_page(top, parent, settings): self.widgets[key].pack(packAttrs) # Sprite Selection + # This one's more-complicated, build it and stuff it self.spriteNameVar2 = StringVar() spriteDialogFrame2 = Frame(self.frames["leftAdjustFrame"]) baseSpriteLabel2 = Label(spriteDialogFrame2, text='Sprite:') @@ -65,6 +69,8 @@ def adjust_page(top, parent, settings): spriteSelectButton2.pack(side=LEFT) spriteDialogFrame2.pack(anchor=E) + # Path to game file to Adjust + # This one's more-complicated, build it and stuff it adjustRomFrame = Frame(self.frames["bottomAdjustFrame"]) adjustRomLabel = Label(adjustRomFrame, text='Rom to adjust: ') self.romVar2 = StringVar(value=settings["rom"]) @@ -82,6 +88,7 @@ def adjust_page(top, parent, settings): romSelectButton2.pack(side=LEFT) adjustRomFrame.pack(fill=X) + # These are the options to Adjust def adjustRom(): options = { "heartbeep": "heartbeep", diff --git a/source/gui/bottom.py b/source/gui/bottom.py new file mode 100644 index 00000000..218f7785 --- /dev/null +++ b/source/gui/bottom.py @@ -0,0 +1,273 @@ +from tkinter import ttk, messagebox, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT, X +from argparse import Namespace +import logging +import os +import random +import re +from CLI import parse_cli +from Fill import FillError +from Main import main, EnemizerError +from Utils import local_path, output_path, open_file, update_deprecated_args +import source.classes.constants as CONST +from source.gui.randomize.multiworld import multiworld_page +import source.gui.widgets as widgets +from source.classes.Empty import Empty + + +def bottom_frame(self, parent, args=None): + # Bottom Frame + self = ttk.Frame(parent) + + # Bottom Frame options + self.widgets = {} + + mw,_ = multiworld_page(self, parent.settings) + mw.pack(fill=X, expand=True) + self.widgets = mw.widgets + + # 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.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.widgets["seed"].storageVar.get() + parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None + self.widgets[widget].storageVar.trace_add("write",saveSeed) + # frame: pack + self.widgets[widget].pieces["frame"].pack(side=LEFT) + + ## Number of Generation attempts + key = "generationcount" + self.widgets[key] = widgets.make_widget( + self, + "spinbox", + self, + "Count", + None, + None, + {"label": {"side": LEFT}, "spinbox": {"side": RIGHT}} + ) + self.widgets[key].pack(side=LEFT) + + def generateRom(): + guiargs = create_guiargs(parent) + # get default values for missing parameters + for k,v in vars(parse_cli(['--multi', str(guiargs.multi)])).items(): + if k not in vars(guiargs): + setattr(guiargs, k, v) + elif type(v) is dict: # use same settings for every player + setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)}) + argsDump = vars(guiargs) + hasEnemizer = "enemizercli" in argsDump and os.path.isfile(argsDump["enemizercli"]) + needEnemizer = False + if not hasEnemizer: + falsey = [ "none", "default", "vanilla", False, 0 ] + for enemizerOption in [ "shufflepots", "shuffleenemies", "enemy_damage", "shufflebosses", "enemy_health" ]: + if enemizerOption in argsDump: + if isinstance(argsDump[enemizerOption], dict): + for playerID,playerSetting in argsDump[enemizerOption].items(): + if not playerSetting in falsey: + needEnemizer = True + elif not argsDump[enemizerOption] in falsey: + needEnemizer = True + seeds = [] + if not needEnemizer or (needEnemizer and hasEnemizer): + try: + if guiargs.count is not None and guiargs.seed: + seed = guiargs.seed + for _ in range(guiargs.count): + seeds.append(seed) + main(seed=seed, args=guiargs, fish=parent.fish) + seed = random.randint(0, 999999999) + else: + if guiargs.seed: + seeds.append(guiargs.seed) + else: + random.seed(None) + guiargs.seed = random.randint(0, 999999999) + seeds.append(guiargs.seed) + main(seed=guiargs.seed, args=guiargs, fish=parent.fish) + except (FillError, EnemizerError, Exception, RuntimeError) as e: + logging.exception(e) + messagebox.showerror(title="Error while creating seed", message=str(e)) + else: + YES = parent.fish.translate("cli","cli","yes") + NO = parent.fish.translate("cli","cli","no") + successMsg = "" + made = {} + for k in [ "rom", "playthrough", "spoiler" ]: + made[k] = parent.fish.translate("cli","cli","made." + k) + for k in made: + v = made[k] + pattern = "([\w]+)(:)([\s]+)(.*)" + m = re.search(pattern,made[k]) + made[k] = m.group(1) + m.group(2) + ' ' + m.group(4) + successMsg += (made["rom"] % (YES if (guiargs.create_rom) else NO)) + "\n" + successMsg += (made["playthrough"] % (YES if (guiargs.calc_playthrough) else NO)) + "\n" + successMsg += (made["spoiler"] % (YES if (not guiargs.jsonout and guiargs.create_spoiler) else NO)) + "\n" + # FIXME: English + successMsg += ("Seed%s: %s" % ('s' if len(seeds) > 1 else "", ','.join(str(x) for x in seeds))) + + messagebox.showinfo(title="Success", message=successMsg) + + ## Generate Button + # 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: + open_file(output_path(args.outputpath)) + else: + open_file(output_path(parent.settings["outputpath"])) + + ## 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')) + 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 + + +def create_guiargs(parent): + guiargs = Namespace() + + # set up settings to gather + # Page::Subpage::GUI-id::param-id + options = CONST.SETTINGSTOPROCESS + + # Cycle through each page + for mainpage in options: + # Cycle through each subpage (in case of Item Randomizer) + 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] + setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get()) + + # Get EnemizerCLI setting + guiargs.enemizercli = parent.pages["randomizer"].pages["enemizer"].widgets["enemizercli"].storageVar.get() + + # Get Multiworld Worlds count + guiargs.multi = int(parent.pages["bottom"].pages["content"].widgets["worlds"].storageVar.get()) + + # Get baserom path + 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 = 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 = 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 = { + "nobgm": "disablemusic", + "quickswap": "quickswap", + "heartcolor": "heartcolor", + "heartbeep": "heartbeep", + "menuspeed": "fastmenu", + "owpalettes": "ow_palettes", + "uwpalettes": "uw_palettes" + } + for adjustarg in adjustargs: + internal = adjustargs[adjustarg] + setattr(guiargs,"adjust." + internal, parent.pages["adjust"].content.widgets[adjustarg].storageVar.get()) + + # Get Custom Items and Starting Inventory Items + customitems = CONST.CUSTOMITEMS + guiargs.startinventory = [] + guiargs.customitemarray = {} + guiargs.startinventoryarray = {} + for customitem in customitems: + if customitem not in CONST.CANTSTARTWITH: + # Starting Inventory is a CSV + amount = int(parent.pages["startinventory"].content.startingWidgets[customitem].storageVar.get()) + guiargs.startinventoryarray[customitem] = amount + for _ in range(0, amount): + label = CONST.CUSTOMITEMLABELS[customitems.index(customitem)] + guiargs.startinventory.append(label) + # Custom Item Pool is a dict of ints + guiargs.customitemarray[customitem] = int(parent.pages["custom"].content.customWidgets[customitem].storageVar.get()) + + # Starting Inventory is a CSV + guiargs.startinventory = ','.join(guiargs.startinventory) + + # Get Sprite Selection (set or random) + guiargs.sprite = parent.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"] + guiargs.randomSprite = parent.randomSprite.get() + + # Get output path + guiargs.outputpath = parent.outputPath.get() + + guiargs = update_deprecated_args(guiargs) + + return guiargs diff --git a/source/gui/custom/__init__.py b/source/gui/custom/__init__.py new file mode 100644 index 00000000..febb4d06 --- /dev/null +++ b/source/gui/custom/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.gui.custom" package diff --git a/gui/custom/overview.py b/source/gui/custom/overview.py similarity index 71% rename from gui/custom/overview.py rename to source/gui/custom/overview.py index 2c37a8bb..c51c35be 100644 --- a/gui/custom/overview.py +++ b/source/gui/custom/overview.py @@ -1,25 +1,27 @@ -from tkinter import ttk, Frame, N, LEFT, VERTICAL, Y -import gui.widgets as widgets +from tkinter import ttk, Frame, N, E, W, LEFT, X, VERTICAL, Y +import source.gui.widgets as widgets import json import os -import classes.constants as CONST +import source.classes.constants as CONST - -def custom_page(top, parent): +def custom_page(top,parent): # Custom Item Pool self = ttk.Frame(parent) + # Create uniform list columns def create_list_frame(parent, framename): parent.frames[framename] = Frame(parent) parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N) parent.frames[framename].thisRow = 0 parent.frames[framename].thisCol = 0 + # Create a vertical rule to help with splitting columns visually def create_vertical_rule(num=1): - for i in range(0,num): + for _ in range(0,num): ttk.Separator(self, orient=VERTICAL).pack(side=LEFT, anchor=N, fill=Y) + # This was in here, I have no idea what it was but I left it just in case: MikeT def validation(P): if str.isdigit(P) or P == "": return True @@ -32,6 +34,7 @@ def custom_page(top, parent): # Custom Item Pool option sections self.frames = {} + # Create 5 columns with 2 vertical rules in between each create_list_frame(self, "itemList1") create_vertical_rule(2) create_list_frame(self, "itemList2") @@ -42,6 +45,8 @@ def custom_page(top, parent): create_vertical_rule(2) create_list_frame(self, "itemList5") + # Load Custom option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes with open(os.path.join("resources", "app", "gui", "custom", "overview", "widgets.json")) as widgetDefns: myDict = json.load(widgetDefns) for framename,theseWidgets in myDict.items(): @@ -49,6 +54,7 @@ def custom_page(top, parent): for key in dictWidgets: self.customWidgets[key] = dictWidgets[key] + # Load Custom Item Pool settings from settings file for key in CONST.CUSTOMITEMS: self.customWidgets[key].storageVar.set(top.settings["customitemarray"][key]) diff --git a/source/gui/loadcliargs.py b/source/gui/loadcliargs.py new file mode 100644 index 00000000..31658549 --- /dev/null +++ b/source/gui/loadcliargs.py @@ -0,0 +1,208 @@ +from source.classes.SpriteSelector import SpriteSelector as spriteSelector +from source.gui.randomize.gameoptions import set_sprite +from Rom import Sprite, get_sprite_from_name +from Utils import update_deprecated_args +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: + args = update_deprecated_args(args) + args = vars(args) + fish = BabelFish() + for k, v in args.items(): + if isinstance(v,dict) and 1 in v: + setattr(args, k, v[1]) # only get values for player 1 for now + # load values from commandline args + + # set up options to get + # Page::Subpage::GUI-id::param-id + options = CONST.SETTINGSTOPROCESS + + # Cycle through each page + for mainpage in options: + # Cycle through each subpage (in case of Item Randomizer) + for subpage in options[mainpage]: + # Cycle through each widget + for widget in options[mainpage][subpage]: + 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 + '.' + str(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 + 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 + 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 + mainpage = "bottom" + subpage = "content" + 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 = "bottom" + subpage = "content" + 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 + mainpage = "bottom" + subpage = "content" + widget = "seed" + setting = "seed" + if args[setting]: + 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) + + # Get number of generations to run + 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.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): + gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"] = spriteObject + if args["sprite"] is not None: + sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"]) + set_sprite(sprite_obj, False, spriteSetter=sprite_setter, + spriteNameVar=gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteNameVar"], + randomSpriteVar=gui.randomSprite) + + def sprite_setter_adj(spriteObject): + gui.pages["adjust"].content.sprite = spriteObject + if args["sprite"] is not None: + sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"]) + set_sprite(sprite_obj, False, spriteSetter=sprite_setter_adj, + spriteNameVar=gui.pages["adjust"].content.spriteNameVar2, + randomSpriteVar=gui.randomSprite) + +# Load args/settings for Adjust tab +def loadadjustargs(gui, settings): + options = { + "adjust": { + "content": { + "nobgm": "adjust.nobgm", + "quickswap": "adjust.quickswap", + "heartcolor": "adjust.heartcolor", + "heartbeep": "adjust.heartbeep", + "menuspeed": "adjust.menuspeed", + "owpalettes": "adjust.owpalettes", + "uwpalettes": "adjust.uwpalettes" + } + } + } + for mainpage in options: + for subpage in options[mainpage]: + for widget in options[mainpage][subpage]: + key = options[mainpage][subpage][widget] + if key in settings: + gui.pages[mainpage].content.widgets[widget].storageVar.set(settings[key]) diff --git a/source/gui/randomize/__init__.py b/source/gui/randomize/__init__.py new file mode 100644 index 00000000..564c2ab5 --- /dev/null +++ b/source/gui/randomize/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.gui.randomize" package diff --git a/gui/randomize/dungeon.py b/source/gui/randomize/dungeon.py similarity index 75% rename from gui/randomize/dungeon.py rename to source/gui/randomize/dungeon.py index 1d575521..01985982 100644 --- a/gui/randomize/dungeon.py +++ b/source/gui/randomize/dungeon.py @@ -1,5 +1,5 @@ -from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT -import gui.widgets as widgets +from tkinter import ttk, Frame, Label, E, W, LEFT, RIGHT +import source.gui.widgets as widgets import json import os @@ -19,17 +19,23 @@ def dungeon_page(parent): mscbLabel = Label(self.frames["keysanity"], text="Shuffle: ") mscbLabel.pack(side=LEFT) + # Load Dungeon Shuffle option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + # This first set goes in the Keysanity frame with open(os.path.join("resources","app","gui","randomize","dungeon","keysanity.json")) as keysanityItems: myDict = json.load(keysanityItems) + myDict = myDict["keysanity"] dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"]) for key in dictWidgets: self.widgets[key] = dictWidgets[key] self.widgets[key].pack(side=LEFT) + # These get split left & right self.frames["widgets"] = Frame(self) self.frames["widgets"].pack(anchor=W) with open(os.path.join("resources","app","gui","randomize","dungeon","widgets.json")) as dungeonWidgets: myDict = json.load(dungeonWidgets) + myDict = myDict["widgets"] dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"]) for key in dictWidgets: self.widgets[key] = dictWidgets[key] diff --git a/source/gui/randomize/enemizer.py b/source/gui/randomize/enemizer.py new file mode 100644 index 00000000..f6ba1846 --- /dev/null +++ b/source/gui/randomize/enemizer.py @@ -0,0 +1,89 @@ +from tkinter import ttk, filedialog, StringVar, Button, Entry, Frame, Label, N, E, W, LEFT, RIGHT, BOTTOM, X +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): + webbrowser.open("https://github.com/Bonta0/Enemizer/releases") + + # Enemizer + self = ttk.Frame(parent) + + # Enemizer options + self.widgets = {} + + # Enemizer option sections + self.frames = {} + + # Enemizer option frames + self.frames["checkboxes"] = Frame(self) + self.frames["checkboxes"].pack(anchor=W) + + self.frames["selectOptionsFrame"] = Frame(self) + self.frames["leftEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"]) + self.frames["rightEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"]) + self.frames["bottomEnemizerFrame"] = Frame(self) + self.frames["selectOptionsFrame"].pack(fill=X) + self.frames["leftEnemizerFrame"].pack(side=LEFT) + self.frames["rightEnemizerFrame"].pack(side=RIGHT) + self.frames["bottomEnemizerFrame"].pack(fill=X) + + # Load Enemizer option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + # These get split left & right + with open(os.path.join("resources","app","gui","randomize","enemizer","widgets.json")) as widgetDefns: + myDict = json.load(widgetDefns) + for framename,theseWidgets in myDict.items(): + dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename]) + for key in dictWidgets: + self.widgets[key] = dictWidgets[key] + packAttrs = {"anchor":E} + if self.widgets[key].type == "checkbox": + packAttrs["anchor"] = W + self.widgets[key].pack(packAttrs) + + ## Enemizer CLI Path + # This one's more-complicated, build it and stuff it + # 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.widgets[widget].storageVar.set(path) + settings["enemizercli"] = path + # 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/gui/randomize/entrando.py b/source/gui/randomize/entrando.py similarity index 72% rename from gui/randomize/entrando.py rename to source/gui/randomize/entrando.py index 3ad6bac4..0759218c 100644 --- a/gui/randomize/entrando.py +++ b/source/gui/randomize/entrando.py @@ -1,5 +1,5 @@ -from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT -import gui.widgets as widgets +from tkinter import ttk, Frame, E, W, LEFT, RIGHT +import source.gui.widgets as widgets import json import os @@ -15,6 +15,11 @@ def entrando_page(parent): self.frames["widgets"] = Frame(self) self.frames["widgets"].pack(anchor=W) + # Load Entrance Randomizer option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + # Checkboxes go West + # Everything else goes East + # They also get split left & right with open(os.path.join("resources","app","gui","randomize","entrando","widgets.json")) as widgetDefns: myDict = json.load(widgetDefns) for framename,theseWidgets in myDict.items(): diff --git a/gui/randomize/gameoptions.py b/source/gui/randomize/gameoptions.py similarity index 84% rename from gui/randomize/gameoptions.py rename to source/gui/randomize/gameoptions.py index eb107df4..46fda67a 100644 --- a/gui/randomize/gameoptions.py +++ b/source/gui/randomize/gameoptions.py @@ -1,7 +1,7 @@ -from tkinter import ttk, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, OptionMenu, E, W, LEFT, RIGHT +from tkinter import ttk, StringVar, Button, Entry, Frame, Label, E, W, LEFT, RIGHT from functools import partial -import classes.SpriteSelector as spriteSelector -import gui.widgets as widgets +import source.classes.SpriteSelector as spriteSelector +import source.gui.widgets as widgets import json import os @@ -17,11 +17,17 @@ def gameoptions_page(top, parent): self.frames["checkboxes"] = Frame(self) self.frames["checkboxes"].pack(anchor=W) + # Game Options frames self.frames["leftRomOptionsFrame"] = Frame(self) self.frames["rightRomOptionsFrame"] = Frame(self) self.frames["leftRomOptionsFrame"].pack(side=LEFT) self.frames["rightRomOptionsFrame"].pack(side=RIGHT) + # Load Game Options widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + # Checkboxes go West + # Everything else goes East + # They also get split left & right with open(os.path.join("resources","app","gui","randomize","gameoptions","widgets.json")) as widgetDefns: myDict = json.load(widgetDefns) for framename,theseWidgets in myDict.items(): @@ -34,6 +40,7 @@ def gameoptions_page(top, parent): self.widgets[key].pack(packAttrs) ## Sprite selection + # This one's more-complicated, build it and stuff it spriteDialogFrame = Frame(self.frames["leftRomOptionsFrame"]) baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:') @@ -75,4 +82,3 @@ def set_sprite(sprite_param, random_sprite=False, spriteSetter=None, spriteNameV spriteNameVar.set(sprite_param.name) if randomSpriteVar: randomSpriteVar.set(random_sprite) - diff --git a/source/gui/randomize/generation.py b/source/gui/randomize/generation.py new file mode 100644 index 00000000..f358c864 --- /dev/null +++ b/source/gui/randomize/generation.py @@ -0,0 +1,79 @@ +from tkinter import ttk, filedialog, StringVar, Button, Entry, Frame, Label, E, W, LEFT, X +import source.gui.widgets as widgets +import json +import os +from source.classes.Empty import Empty + +def generation_page(parent,settings): + # Generation Setup + self = ttk.Frame(parent) + + # Generation Setup options + self.widgets = {} + + # Generation Setup option sections + self.frames = {} + self.frames["checkboxes"] = Frame(self) + self.frames["checkboxes"].pack(anchor=W) + + # Load Generation Setup option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + with open(os.path.join("resources","app","gui","randomize","generation","checkboxes.json")) as checkboxes: + myDict = json.load(checkboxes) + myDict = myDict["checkboxes"] + dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["checkboxes"]) + for key in dictWidgets: + self.widgets[key] = dictWidgets[key] + self.widgets[key].pack(anchor=W) + + self.frames["widgets"] = Frame(self) + self.frames["widgets"].pack(anchor=W) + # Load Generation Setup option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + with open(os.path.join("resources","app","gui","randomize","generation","widgets.json")) as items: + myDict = json.load(items) + myDict = myDict["widgets"] + dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"]) + for key in dictWidgets: + self.widgets[key] = dictWidgets[key] + self.widgets[key].pack(anchor=W) + + self.frames["baserom"] = Frame(self) + self.frames["baserom"].pack(anchor=W, fill=X) + ## Locate base ROM + # This one's more-complicated, build it and stuff it + # 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.widgets[widget].storageVar.set(rom) + # dialog button + self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"], text='Select Rom', command=RomSelect) + + # 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/gui/randomize/item.py b/source/gui/randomize/item.py similarity index 76% rename from gui/randomize/item.py rename to source/gui/randomize/item.py index f962d574..b01892ab 100644 --- a/gui/randomize/item.py +++ b/source/gui/randomize/item.py @@ -1,8 +1,8 @@ -from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT -import gui.widgets as widgets +from tkinter import ttk, Frame, E, W, LEFT, RIGHT +import source.gui.widgets as widgets import json import os - + def item_page(parent): # Item Randomizer self = ttk.Frame(parent) @@ -13,6 +13,7 @@ def item_page(parent): # Item Randomizer option sections self.frames = {} + # Item Randomizer option frames self.frames["checkboxes"] = Frame(self) self.frames["checkboxes"].pack(anchor=W) @@ -21,6 +22,10 @@ def item_page(parent): self.frames["leftItemFrame"].pack(side=LEFT) self.frames["rightItemFrame"].pack(side=RIGHT) + # Load Item Randomizer option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + # Checkboxes go West + # Everything else goes East with open(os.path.join("resources","app","gui","randomize","item","widgets.json")) as widgetDefns: myDict = json.load(widgetDefns) for framename,theseWidgets in myDict.items(): diff --git a/source/gui/randomize/multiworld.py b/source/gui/randomize/multiworld.py new file mode 100644 index 00000000..493f31b5 --- /dev/null +++ b/source/gui/randomize/multiworld.py @@ -0,0 +1,60 @@ +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 + self = ttk.Frame(parent) + + # Multiworld options + self.widgets = {} + + # Multiworld option sections + self.frames = {} + self.frames["widgets"] = Frame(self) + self.frames["widgets"].pack(anchor=W, fill=X) + + # Load Multiworld option widgets as defined by JSON file + # Defns include frame name, widget type, widget options, widget placement attributes + with open(os.path.join("resources","app","gui","randomize","multiworld","widgets.json")) as multiworldItems: + myDict = json.load(multiworldItems) + myDict = myDict["widgets"] + dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"]) + for key in dictWidgets: + self.widgets[key] = dictWidgets[key] + self.widgets[key].pack(side=LEFT, anchor=N) + + ## List of Player Names + # This one's more-complicated, build it and stuff it + # 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["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/startinventory/__init__.py b/source/gui/startinventory/__init__.py new file mode 100644 index 00000000..024e33ee --- /dev/null +++ b/source/gui/startinventory/__init__.py @@ -0,0 +1 @@ +# do nothing, just exist to make "source.gui.startinventory" package diff --git a/gui/startinventory/overview.py b/source/gui/startinventory/overview.py similarity index 73% rename from gui/startinventory/overview.py rename to source/gui/startinventory/overview.py index aaa601b3..fce40e5f 100644 --- a/gui/startinventory/overview.py +++ b/source/gui/startinventory/overview.py @@ -1,24 +1,27 @@ -from tkinter import ttk, StringVar, Entry, Frame, Label, N, E, W, LEFT, RIGHT, X, VERTICAL, Y -import gui.widgets as widgets +from tkinter import ttk, Frame, N, E, W, LEFT, X, VERTICAL, Y +import source.gui.widgets as widgets import json import os -import classes.constants as CONST +import source.classes.constants as CONST def startinventory_page(top,parent): # Starting Inventory self = ttk.Frame(parent) + # Create uniform list columns def create_list_frame(parent, framename): parent.frames[framename] = Frame(parent) parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N) parent.frames[framename].thisRow = 0 parent.frames[framename].thisCol = 0 + # Create a vertical rule to help with splitting columns visually def create_vertical_rule(num=1): - for i in range(0,num): + for _ in range(0,num): ttk.Separator(self, orient=VERTICAL).pack(side=LEFT, anchor=N, fill=Y) + # This was in Custom Item Pool, I have no idea what it was but I left it just in case: MikeT def validation(P): if str.isdigit(P) or P == "": return True @@ -31,6 +34,7 @@ def startinventory_page(top,parent): # Starting Inventory option sections self.frames = {} + # Create 5 columns with 2 vertical rules in between each create_list_frame(self,"itemList1") create_vertical_rule(2) create_list_frame(self,"itemList2") @@ -41,6 +45,8 @@ def startinventory_page(top,parent): create_vertical_rule(2) create_list_frame(self,"itemList5") + # Load Starting Inventory option widgets as defined by JSON file, ignoring the ones to be excluded + # Defns include frame name, widget type, widget options, widget placement attributes with open(os.path.join("resources","app","gui","custom","overview","widgets.json")) as widgetDefns: myDict = json.load(widgetDefns) for key in CONST.CANTSTARTWITH: @@ -53,6 +59,7 @@ def startinventory_page(top,parent): for key in dictWidgets: self.startingWidgets[key] = dictWidgets[key] + # Load Custom Starting Inventory settings from settings file, ignoring ones to be excluded for key in CONST.CUSTOMITEMS: if key not in CONST.CANTSTARTWITH: val = 0 diff --git a/gui/widgets.py b/source/gui/widgets.py similarity index 60% rename from gui/widgets.py rename to source/gui/widgets.py index b4d15557..d3915c6f 100644 --- a/gui/widgets.py +++ b/source/gui/widgets.py @@ -1,8 +1,7 @@ -from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, RIGHT, X - -class Empty(): - pass +from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, LEFT, RIGHT, X +from source.classes.Empty import Empty +# Override Spinbox to include mousewheel support for changing value class mySpinbox(Spinbox): def __init__(self, *args, **kwargs): Spinbox.__init__(self, *args, **kwargs) @@ -16,66 +15,132 @@ class mySpinbox(Spinbox): elif event.num == 4 or event.delta == 120: self.invoke('buttonup') +# Make a Checkbutton with a label def make_checkbox(self, parent, label, storageVar, manager, managerAttrs): - self = Frame(parent, name="checkframe-" + label.lower()) + self = Frame(parent) self.storageVar = storageVar - self.checkbox = Checkbutton(self, text=label, variable=self.storageVar, name="checkbox-" + label.lower()) + if managerAttrs is not None and "default" in managerAttrs: + if managerAttrs["default"] == "true" or managerAttrs["default"] == True: + self.storageVar.set(True) + elif managerAttrs["default"] == "false" or managerAttrs["default"] == False: + self.storageVar.set(False) + del managerAttrs["default"] + self.checkbox = Checkbutton(self, text=label, variable=self.storageVar) if managerAttrs is not None: self.checkbox.pack(managerAttrs) else: self.checkbox.pack() return self +# 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) + self = Frame(parent) + + 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.label.pack(side=LEFT) + 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 managerAttrs is not None and "default" in managerAttrs: + default = managerAttrs["default"] + labels = self.selectbox.options["labels"] + values = self.selectbox.options["values"] + if default in values: + idx = values.index(default) + if not labels[idx] == "": + self.labelVar.set(labels[idx]) + self.selectbox["menu"].entryconfigure(idx,label=labels[idx]) + self.storageVar.set(values[idx]) + if managerAttrs is not None and "selectbox" in managerAttrs: self.selectbox.pack(managerAttrs["selectbox"]) else: - self.selectbox.pack() + self.selectbox.pack(side=RIGHT) return self +# Make a Spinbox with a label, limit 1-100 def make_spinbox(self, parent, label, storageVar, manager, managerAttrs): - self = Frame(parent, name="spinframe-" + label.lower()) + self = Frame(parent) self.storageVar = storageVar self.label = Label(self, text=label) if managerAttrs is not None and "label" in managerAttrs: self.label.pack(managerAttrs["label"]) else: - self.label.pack() + self.label.pack(side=LEFT) fromNum = 1 toNum = 100 - if "spinbox" in managerAttrs: + if managerAttrs is not None and "spinbox" in managerAttrs: if "from" in managerAttrs: fromNum = managerAttrs["spinbox"]["from"] if "to" in managerAttrs: toNum = managerAttrs["spinbox"]["to"] - self.spinbox = mySpinbox(self, from_=fromNum, to=toNum, width=5, textvariable=self.storageVar, name="spinbox-" + label.lower()) + self.spinbox = mySpinbox(self, from_=fromNum, to=toNum, width=5, textvariable=self.storageVar) if managerAttrs is not None and "spinbox" in managerAttrs: self.spinbox.pack(managerAttrs["spinbox"]) else: - self.spinbox.pack() + self.spinbox.pack(side=RIGHT) return self +# Make an Entry box with a label +# Support for Grid or Pack so that the Custom Item Pool & Starting Inventory pages don't look ugly def make_textbox(self, parent, label, storageVar, manager, managerAttrs): widget = Empty() widget.storageVar = storageVar @@ -87,6 +152,8 @@ def make_textbox(self, parent, label, storageVar, manager, managerAttrs): # grid if manager == "grid": widget.label.grid(managerAttrs["label"] if managerAttrs is not None and "label" in managerAttrs else None, row=parent.thisRow, column=parent.thisCol) + if managerAttrs is not None and "label" not in managerAttrs: + widget.label.grid_configure(sticky="w") parent.thisCol += 1 widget.textbox.grid(managerAttrs["textbox"] if managerAttrs is not None and "textbox" in managerAttrs else None, row=parent.thisRow, column=parent.thisCol) parent.thisRow += 1 @@ -98,7 +165,7 @@ def make_textbox(self, parent, label, storageVar, manager, managerAttrs): widget.textbox.pack(managerAttrs["textbox"] if managerAttrs is not None and "textbox" in managerAttrs else None) return widget - +# Make a generic widget def make_widget(self, type, parent, label, storageVar=None, manager=None, managerAttrs=dict(), options=None): widget = None if manager is None: @@ -129,17 +196,26 @@ def make_widget(self, type, parent, label, storageVar=None, manager=None, manage widget.type = type return widget +# Make a generic widget from a dict def make_widget_from_dict(self, defn, parent): type = defn["type"] if "type" in defn else None label = defn["label"]["text"] if "label" in defn and "text" in defn["label"] else "" manager = defn["manager"] if "manager" in defn else None managerAttrs = defn["managerAttrs"] if "managerAttrs" in defn else None options = defn["options"] if "options" in defn else None + + if managerAttrs is None and "default" in defn: + managerAttrs = {} + if "default" in defn: + managerAttrs["default"] = defn["default"] + 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 def make_widgets_from_dict(self, defns, parent): widgets = {} for key,defn in defns.items(): widgets[key] = make_widget_from_dict(self, defn, parent) - return widgets + return widgets diff --git a/test-options.py b/test-options.py new file mode 100644 index 00000000..60386860 --- /dev/null +++ b/test-options.py @@ -0,0 +1,109 @@ +from __future__ import annotations +from aenum import Enum, IntEnum, extend_enum +from source.classes.BabelFish import BabelFish +import json +import os + +fish = BabelFish(lang="en") + +def tokenize(token): + for search,replace in ( + ('(', ""), + (')',""), + ("'", ""), + ('-'," "), + ('/',""), + ("\\","") + ): + token = token.replace(search, replace) + tokens = token.split(" ") + i = 0 + for check in tokens: + if check.lower() == check: + tokens[i] = "" + i += 1 + return " ".join(tokens).replace(" ","") + +class Toggle(IntEnum): + Off = 0 + On = 1 + + @classmethod + def from_text(cls, text: str) -> Toggle: + if text.lower() in {"off", "0", "false", "none", "null"}: + return Toggle.Off + else: + return Toggle.On + +class Choice(IntEnum): + @classmethod + def from_text(cls, text: str) -> Choice: + for option in cls: + if option.name == text.upper(): + return option + raise KeyError( + 'KeyError: Could not find option "%s" for "%s", known options are %s' % + ( + text, + cls.__name__, + ", ".join(option.name for option in cls) + ) + ) + +def create_choice(option_name,option_vals): + option = type(option_name,(Choice,),{}) + for name in option_vals: + extend_enum(option,str(name).upper(),len(option)) + return option + +def load_options(filepath): + theseCompiled = {} + with open(filepath) as widgetsDefn: + filepath = filepath.split(os.sep) + domain = filepath[3] + key = filepath[4] + theseOptions = json.load(widgetsDefn) + for section in theseOptions: + widgets = theseOptions[section] + for widget in widgets: + thisWidget = widgets[widget] + if domain == "randomize": + domain = "randomizer" + if key == "entrando": + key = "entrance" + fish_key = domain + '.' + key + '.' + widget + option_name = tokenize(fish.translate("gui","gui",fish_key,"en")) + if thisWidget["type"] == "checkbox": + theseCompiled[option_name] = Toggle + elif thisWidget["type"] == "selectbox": + option_vals = thisWidget["options"] + theseCompiled[option_name] = create_choice(option_name,option_vals) + return theseCompiled + + +if __name__ == "__main__": + import argparse + + compiledOptions = {} + notebooks = { + "randomize": [ "dungeon", "enemizer", "entrando", "gameoptions", "generation", "item", "multiworld" ] + } + for notebook in notebooks: + for page in notebooks[notebook]: + for filename in ["keysanity","checkboxes","widgets"]: + defn = os.path.join("resources", "app", "gui", notebook, page, filename + ".json") + if os.path.isfile(defn): + compiledOptions.update(load_options(defn)) + + test = argparse.Namespace() + test.logic = compiledOptions["LogicLevel"].from_text("nologic") + test.mapshuffle = compiledOptions["Maps"].from_text("ON") + try: + test.logic = compiledOptions["LogicLevel"].from_text("overworldglitches") + except KeyError as e: + print(e) + if test.mapshuffle: + print("Map Shuffle is on") + print(test) + for option in compiledOptions: + print("%s: %s" % (option, list(compiledOptions[option])))