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])))