Merge branch 'DoorDev' into EdgeWork

This commit is contained in:
aerinon
2020-03-24 10:49:28 -06:00
87 changed files with 3513 additions and 1991 deletions

249
.github/workflows/ci.yml vendored Normal file
View File

@@ -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

4
.gitignore vendored
View File

@@ -19,8 +19,8 @@ RaceRom.py
upx/
weights/
settings.json
working_dirs.json
resources/user/*
!resources/user/.gitkeep
*.exe

View File

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

506
CLI.py
View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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__':

View File

@@ -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=[],

View File

@@ -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()

56
Gui.py
View File

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

View File

@@ -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=[],

View File

@@ -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()

100
Main.py
View File

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

View File

@@ -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

6
RELEASENOTES.md Normal file
View File

@@ -0,0 +1,6 @@
# Features
## Door Randomizer
* Native GUI executables
* Native Dungeon Randomizer CLI executables

10
Rom.py
View File

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

109
Utils.py
View File

@@ -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 += ('<!-- ' + d + ' -->') + "\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('| '+' <br /> '.join(strs_to_print))
print('|}')
strs_to_print.append('{{Dungeon Door|{{PAGENAME}}|' + ext.name + '}}')
toprint += ('| '+'<br />'.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 += ('<!-- ' + d + ' -->') + "\n"
for tile, region_list in tile_map.items():
for region in region_list:
toprint += ('<!-- ' + region.name + ' -->') + "\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')

5
build-app_version.py Normal file
View File

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

View File

@@ -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 ",

View File

@@ -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 ",

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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("<Button-1>", open_enemizer_download)
self.enemizerCLIpathVar = StringVar(value=settings["enemizercli"])
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=self.enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT, fill=X, expand=True)
def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join("."))
if path:
self.enemizerCLIpathVar.set(path)
settings["enemizercli"] = path
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT)
enemizerPathFrame.pack(fill=X)
return self,settings

View File

@@ -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

View File

@@ -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

View File

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

1
resources/__init__.py Normal file
View File

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

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

@@ -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": {}
}

View File

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

View File

@@ -0,0 +1,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)"
]
}
}

View File

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

View File

@@ -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",

View File

@@ -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
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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" }
}
}

View File

@@ -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"
]
}
}
}

View File

@@ -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"
]
}
}
}
}

View File

@@ -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"
]
}
}
}

View File

@@ -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"
]
}
}
}

View File

@@ -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" }
}
}

View File

@@ -0,0 +1,12 @@
{
"widgets": {
"saveonexit": {
"type": "selectbox",
"options": [
"ask",
"always",
"never"
]
}
}
}

View File

@@ -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"
]
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1 @@
aenum

1
resources/ci/__init__.py Normal file
View File

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

View File

@@ -0,0 +1 @@
# do nothing, just exist to make "resources.ci.common" package

View File

@@ -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 <app_version>.<build_number>
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)

View File

@@ -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.")

View File

@@ -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()

View File

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

View File

@@ -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")
)

View File

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

View File

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

1
resources/user/.gitkeep Normal file
View File

@@ -0,0 +1 @@
# do nothing, just exist to make "user" folder

1
source/__init__.py Normal file
View File

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

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

@@ -0,0 +1,112 @@
import json
import locale
import os
class BabelFish():
def __init__(self,subpath=["resources","app","meta"],lang=None):
localization_string = locale.getdefaultlocale()[0] #get set localization
self.locale = localization_string[:2] if lang is None else lang #let caller override localization
self.langs = ["en"] #start with English
if(not self.locale == "en"): #add localization
self.langs.append(self.locale)
self.lang_defns = {} #collect translations
self.add_translation_file() #start with default translation file
self.add_translation_file(["resources","app","cli"]) #add help translation file
self.add_translation_file(["resources","app","gui"]) #add gui label translation file
self.add_translation_file(["resources","user","meta"]) #add user translation file
def add_translation_file(self,subpath=["resources","app","meta"]):
if not isinstance(subpath, list):
subpath = [subpath]
if "lang" not in subpath:
subpath.append("lang") #look in lang folder
subpath = os.path.join(*subpath) #put in path separators
key = subpath.split(os.sep)
for check in ["resources","app","user"]:
if check in key:
key.remove(check)
key = os.path.join(*key) #put in path separators
for lang in self.langs:
if not lang in self.lang_defns:
self.lang_defns[lang] = {}
langs_filename = os.path.join(subpath,lang + ".json") #get filename of translation file
if os.path.isfile(langs_filename): #if we've got a file
with open(langs_filename,encoding="utf-8") as f: #open it
self.lang_defns[lang][key[:key.rfind(os.sep)].replace(os.sep,'.')] = json.load(f) #save translation definitions
else:
pass
# print(langs_filename + " not found for translation!")
def translate(self, domain="", key="", subkey="", 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

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

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

View File

@@ -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("<Button-1>", 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("<Button-1>", open_spritesomething_listing)

View File

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

View File

@@ -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"
}
}
}

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

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

View File

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

View File

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

View File

@@ -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",

273
source/gui/bottom.py Normal file
View File

@@ -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

View File

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

View File

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

208
source/gui/loadcliargs.py Normal file
View File

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

View File

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

View File

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

View File

@@ -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("<Button-1>", open_enemizer_download)
# storage var
self.widgets[widget].storageVar = StringVar(value=settings["enemizercli"])
# textbox
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"], textvariable=self.widgets[widget].storageVar)
self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True)
def EnemizerSelectPath():
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

View File

@@ -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():

View File

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

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

109
test-options.py Normal file
View File

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