Merge unstable into CrossGen

This commit is contained in:
aerinon
2020-04-10 15:17:31 -06:00
106 changed files with 4298 additions and 2170 deletions
+259
View File
@@ -0,0 +1,259 @@
# workflow name
name: Build
# fire on
on:
push:
branches:
- DoorDev
pull_request:
branches:
- DoorDev
# 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
if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease')
# 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
if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease')
# 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
if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease')
# 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
if: contains(github.ref, 'master') || contains(github.ref, 'stable') || contains(github.ref, 'dev') || contains(github.ref, 'DoorRelease')
+8 -2
View File
@@ -1,6 +1,9 @@
.idea .idea
.vscode .vscode
*_Spoiler.txt *_Spoiler.txt
*.bmbp
*.log
*_Spoiler.json
*.pyc *.pyc
*.sfc *.sfc
*.srm *.srm
@@ -18,9 +21,12 @@ EnemizerCLI/
RaceRom.py RaceRom.py
upx/ upx/
weights/ weights/
/MultiMystery/
/Players/
/QUsb2Snes/
settings.json resources/user/*
working_dirs.json !resources/user/.gitkeep
*.exe *.exe
+41 -18
View File
@@ -4,6 +4,7 @@ import logging
import json import json
from collections import OrderedDict, deque, defaultdict from collections import OrderedDict, deque, defaultdict
from source.classes.BabelFish import BabelFish
from EntranceShuffle import door_addresses from EntranceShuffle import door_addresses
from _vendor.collections_extended import bag from _vendor.collections_extended import bag
from Utils import int16_as_bytes from Utils import int16_as_bytes
@@ -71,8 +72,13 @@ class World(object):
self.key_logic = {} self.key_logic = {}
self.pool_adjustment = {} self.pool_adjustment = {}
self.key_layout = defaultdict(dict) self.key_layout = defaultdict(dict)
self.fish = BabelFish()
for player in range(1, players + 1): for player in range(1, players + 1):
# If World State is Retro, set to Open and set Retro flag
if self.mode[player] == "retro":
self.mode[player] = "open"
self.retro[player] = True
def set_player_attr(attr, val): def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {}) set_player_attr('_region_cache', {})
@@ -1774,43 +1780,60 @@ class Spoiler(object):
outfile.write('\n\nDoors:\n\n') outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join( outfile.write('\n'.join(
['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', ['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '',
entry['entrance'], self.world.fish.translate("meta","doors",entry['entrance']),
'<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>',
entry['exit'], self.world.fish.translate("meta","doors",entry['exit']),
'({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for '({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for
entry in self.doors.values()])) entry in self.doors.values()]))
if self.doorTypes: if self.doorTypes:
# doorNames: For some reason these come in combined, somehow need to split on the thing to translate
# doorTypes: Small Key, Bombable, Bonkable
outfile.write('\n\nDoor Types:\n\n') outfile.write('\n\nDoor Types:\n\n')
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()])) outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta","doors",entry['doorNames']), self.world.fish.translate("meta","doorTypes",entry['type'])) for entry in self.doorTypes.values()]))
if self.entrances: if self.entrances:
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
outfile.write('\n\nEntrances:\n\n') outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()])) outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()]))
outfile.write('\n\nMedallions:\n') outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items(): for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}') outfile.write(f'\n{dungeon}: {medallion} Medallion')
if self.startinventory: if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n') outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory)) outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write('\n\nPaths:\n\n')
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta","locations",location), self.world.fish.translate("meta","items",item)) for grouping in self.locations.values() for (location, item) in grouping.items()]))
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta","locations",shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta","items",item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (self.world.fish.translate("meta","locations",location), self.world.fish.translate("meta","items",item)) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
# items: Item names
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta","items",unreachable.item), self.world.fish.translate("meta","locations",unreachable)) for unreachable in self.unreachables]))
# rooms: Change up room names; only if it's got no locations in it
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
outfile.write('\n\nPaths:\n\n')
path_listings = [] path_listings = []
for location, path in sorted(self.paths.items()): for location, path in sorted(self.paths.items()):
path_lines = [] path_lines = []
for region, exit in path: for region, exit in path:
if exit is not None: if exit is not None:
path_lines.append("{} -> {}".format(region, exit)) path_lines.append("{} -> {}".format(self.world.fish.translate("meta","rooms",region), self.world.fish.translate("meta","entrances",exit)))
else: else:
path_lines.append(region) path_lines.append(self.world.fish.translate("meta","rooms",region))
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) path_listings.append("{}\n {}".format(self.world.fish.translate("meta","locations",location), "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))
+164 -350
View File
@@ -8,11 +8,10 @@ import textwrap
import shlex import shlex
import sys import sys
from Main import main import source.classes.constants as CONST
from Utils import is_bundled, close_console from source.classes.BabelFish import BabelFish
from Fill import FillError
import classes.constants as CONST from Utils import update_deprecated_args
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -20,12 +19,15 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action): def _get_help_string(self, action):
return textwrap.dedent(action.help) return textwrap.dedent(action.help)
def parse_arguments(argv, no_defaults=False): def parse_cli(argv, no_defaults=False):
def defval(value): def defval(value):
return value if not no_defaults else None return value if not no_defaults else None
# get settings # get settings
settings = get_settings() settings = parse_settings()
lang = "en"
fish = BabelFish(lang=lang)
# we need to know how many players we have first # we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
@@ -33,265 +35,45 @@ def parse_arguments(argv, no_defaults=False):
multiargs, _ = parser.parse_known_args(argv) multiargs, _ = parser.parse_known_args(argv)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', default=defval(settings["create_spoiler"] != 0), help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default=defval(settings["logic"]), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'], # get args
help='''\ args = []
Select Enforcement of Item Requirements. (default: %(default)s) with open(os.path.join("resources","app","cli","args.json")) as argsFile:
No Glitches: args = json.load(argsFile)
Minor Glitches: May require Fake Flippers, Bunny Revival for arg in args:
and Dark Room Navigation. argdata = args[arg]
No Logic: Distribute items without regard for argname = "--" + arg
item requirements. argatts = {}
''') argatts["help"] = "(default: %(default)s)"
parser.add_argument('--mode', default=defval(settings["mode"]), const='open', nargs='?', choices=['standard', 'open', 'inverted'], if "action" in argdata:
help='''\ argatts["action"] = argdata["action"]
Select game mode. (default: %(default)s) if "choices" in argdata:
Open: World starts with Zelda rescued. argatts["choices"] = argdata["choices"]
Standard: Fixes Hyrule Castle Secret Entrance and Front Door argatts["const"] = argdata["choices"][0]
but may lead to weird rain state issues if you exit argatts["default"] = argdata["choices"][0]
through the Hyrule Castle side exits before rescuing argatts["nargs"] = "?"
Zelda in a full shuffle. if arg in settings:
Inverted: Starting locations are Dark Sanctuary in West Dark default = settings[arg]
World or at Link's House, which is shuffled freely. if "type" in argdata and argdata["type"] == "bool":
Requires the moon pearl to be Link in the Light World default = settings[arg] != 0
instead of a bunny. argatts["default"] = defval(default)
''') arghelp = fish.translate("cli","help",arg)
parser.add_argument('--swords', default=defval(settings["swords"]), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'], if "help" in argdata and argdata["help"] == "suppress":
help='''\ argatts["help"] = argparse.SUPPRESS
Select sword placement. (default: %(default)s) elif not isinstance(arghelp,str):
Random: All swords placed randomly. argatts["help"] = '\n'.join(arghelp).replace("\\'","'")
Assured: Start game with a sword already. else:
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s argatts["help"] = arghelp + " " + argatts["help"]
Tower are removed, Agahnim\'s Tower barrier can be parser.add_argument(argname,**argatts)
destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon. 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)
Ether and Bombos Tablet can be activated with Hammer 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)
(and Book). Bombos pads have been added in Ice parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS)
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
parser.add_argument('--goal', default=defval(settings["goal"]), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
help='''\
Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then
defeat Ganon.
Crystals: Collect all crystals then defeat Ganon.
Pedestal: Places the Triforce at the Master Sword Pedestal.
All Dungeons: Collect all crystals, pendants, beat both
Agahnim fights and then defeat Ganon.
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game.
''')
parser.add_argument('--difficulty', default=defval(settings["difficulty"]), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select game difficulty. Affects available itempool. (default: %(default)s)
Normal: Normal difficulty.
Hard: A harder setting with less equipment and reduced health.
Expert: A harder yet setting with minimum equipment and health.
''')
parser.add_argument('--item_functionality', default=defval(settings["item_functionality"]), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select limits on item functionality to increase difficulty. (default: %(default)s)
Normal: Normal functionality.
Hard: Reduced functionality.
Expert: Greatly reduced functionality.
''')
parser.add_argument('--timer', default=defval(settings["timer"]), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
help='''\
Select game timer setting. Affects available itempool. (default: %(default)s)
None: No timer.
Display: Displays a timer but does not affect
the itempool.
Timed: Starts with clock at zero. Green Clocks
subtract 4 minutes (Total: 20), Blue Clocks
subtract 2 minutes (Total: 10), Red Clocks add
2 minutes (Total: 10). Winner is player with
lowest time at the end.
Timed OHKO: Starts clock at 10 minutes. Green Clocks add
5 minutes (Total: 25). As long as clock is at 0,
Link will die in one hit.
OHKO: Like Timed OHKO, but no clock items are present
and the clock is permenantly at zero.
Timed Countdown: Starts with clock at 40 minutes. Same clocks as
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
parser.add_argument('--progressive', default=defval(settings["progressive"]), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default=defval(settings["algorithm"]), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
help='''\
Select item filling algorithm. (default: %(default)s
balanced: vt26 derivative that aims to strike a balance between
the overworld heavy vt25 and the dungeon heavy vt26
algorithm.
vt26: Shuffle items and place them in a random location
that it is not impossible to be in. This includes
dungeon keys and items.
vt25: Shuffle items and place them in a random location
that it is not impossible to be in.
vt21: Unbiased in its selection, but has tendency to put
Ice Rod in Turtle Rock.
vt22: Drops off stale locations after 1/3 of progress
items were placed to try to circumvent vt21\'s
shortcomings.
Freshness: Keep track of stale locations (ones that cannot be
reached yet) and decrease likeliness of selecting
them the more often they were found unreachable.
Flood: Push out items starting from Link\'s House and
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval(settings["shuffle"]), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
multi-entrance caves to one world.
Simple: Shuffle Dungeon Entrances/Exits between each other
and keep all 4-entrance dungeons confined to one
location. All caves outside of death mountain are
shuffled in pairs and matched by original type.
Restricted: Use Dungeons shuffling from Simple but freely
connect remaining entrances.
Crossed: Mix cave and dungeon entrances freely while allowing
caves to cross between worlds.
Insanity: Decouple entrances and exits from each other and
shuffle them freely. Caves that used to be single
entrance will still exit to the same location from
which they are entered.
Vanilla: All entrances are in the same locations they were
in the base game.
Legacy shuffles preserve behavior from older versions of the
entrance randomizer including significant technical limitations.
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--door_shuffle', default=defval(settings["door_shuffle"]), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed'],
help='''\
Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon.
(Not yet implemented)
Crossed: Doors are mixed between all dungeons.
(Not yet implemented)
Vanilla: All doors are connected the same way they were in the
base game.
''')
parser.add_argument('--experimental', default=defval(settings["experimental"] != 0), help='Enable experimental features', action='store_true')
parser.add_argument('--dungeon_counters', default=defval(settings["dungeon_counters"]), help='Enable dungeon chest counters', const='off', nargs='?', choices=['off', 'on', 'pickup', 'default'])
parser.add_argument('--crystals_ganon', default=defval(settings["crystals_ganon"]), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to defeat ganon. Any other
requirements for ganon for the selected goal still apply.
This setting does not apply when the all dungeons goal is
selected. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--crystals_gt', default=defval(settings["crystals_gt"]), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to open GT. For inverted mode
this applies to the castle tower door instead. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--openpyramid', default=defval(settings["openpyramid"] != 0), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
''', action='store_true')
parser.add_argument('--rom', default=defval(settings["rom"]), help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--seed', default=defval(int(settings["seed"]) if settings["seed"] != "" and settings["seed"] is not None else None), help='Define seed number to generate.', type=int)
parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else None), help='''\
Use to batch generate multiple seeds with same settings.
If --seed is provided, it will be used for the first seed, then
used to derive the next seed (i.e. generating 10 seeds with
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', default=defval(settings["fastmenu"]), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', default=defval(settings["quickswap"] != 0), help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', default=defval(settings["disablemusic"] != 0), help='Disables game music.', action='store_true')
parser.add_argument('--mapshuffle', default=defval(settings["mapshuffle"] != 0), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--compassshuffle', default=defval(settings["compassshuffle"] != 0), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', default=defval(settings["keyshuffle"] != 0), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', default=defval(settings["bigkeyshuffle"] != 0), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keysanity', default=defval(settings["keysanity"] != 0), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(settings["retro"] != 0), help='''\
Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1.
''', action='store_true')
parser.add_argument('--startinventory', default=defval(settings["startinventory"]), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--usestartinventory', default=defval(settings["usestartinventory"] != 0), help='Not supported.')
parser.add_argument('--custom', default=defval(settings["custom"] != 0), help='Not supported.')
parser.add_argument('--customitemarray', default={}, help='Not supported.')
parser.add_argument('--accessibility', default=defval(settings["accessibility"]), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about
reaching all locations or all keys.
Locations: You will be able to reach every location in the game.
None: You will be able to reach enough locations to beat the game.
''')
parser.add_argument('--hints', default=defval(settings["hints"] != 0), help='''\
Make telepathic tiles and storytellers give helpful hints.
''', action='store_true')
# included for backwards compatibility # included for backwards compatibility
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(settings["shuffleganon"] != 0))
parser.add_argument('--no-shuffleganon', help='''\
If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool.
''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default=defval(settings["heartbeep"]), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default=defval(settings["heartcolor"]), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval(settings["ow_palettes"]), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval(settings["uw_palettes"]), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', default=defval(settings["sprite"]), help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
or 0x7078 (28792) bytes including palette data.
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--suppress_rom', default=defval(settings["suppress_rom"] != 0), help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--jsonout', action='store_true', help='''\
Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise.
''')
parser.add_argument('--skip_playthrough', action='store_true', default=defval(settings["skip_playthrough"] != 0))
parser.add_argument('--enemizercli', default=defval(settings["enemizercli"]))
parser.add_argument('--shufflebosses', default=defval(settings["shufflebosses"]), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=defval(settings["shuffleenemies"]), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval(settings["enemy_health"]), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default=defval(settings["enemy_damage"]), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepots', default=defval(settings["shufflepots"] != 0), action='store_true')
parser.add_argument('--beemizer', default=defval(settings["beemizer"]), type=lambda value: min(max(int(value), 0), 4)) parser.add_argument('--beemizer', default=defval(settings["beemizer"]), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--remote_items', default=defval(settings["remote_items"] != 0), action='store_true')
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(settings["names"]))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath', default=defval(settings["outputpath"]))
parser.add_argument('--race', default=defval(settings["race"] != 0), action='store_true')
parser.add_argument('--saveonexit', default=defval(settings["saveonexit"]), choices=['never', 'ask', 'always'])
parser.add_argument('--outputname')
if multiargs.multi: if multiargs.multi:
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
@@ -305,7 +87,7 @@ def parse_arguments(argv, no_defaults=False):
if multiargs.multi: if multiargs.multi:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1): 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', for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid', 'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
@@ -323,9 +105,10 @@ def parse_arguments(argv, no_defaults=False):
return ret return ret
def get_settings(): def parse_settings():
# set default settings # set default settings
settings = { settings = {
"lang": "en",
"retro": False, "retro": False,
"mode": "open", "mode": "open",
"logic": "noglitches", "logic": "noglitches",
@@ -340,8 +123,9 @@ def get_settings():
"accessibility": "items", "accessibility": "items",
"algorithm": "balanced", "algorithm": "balanced",
# Shuffle Ganon defaults to TRUE
"openpyramid": False, "openpyramid": False,
"shuffleganon": False, "shuffleganon": True,
"shuffle": "vanilla", "shuffle": "vanilla",
"shufflepots": False, "shufflepots": False,
@@ -363,105 +147,111 @@ def get_settings():
"multi": 1, "multi": 1,
"names": "", "names": "",
# Hints default to TRUE
"hints": True, "hints": True,
"no_hints": False,
"disablemusic": False, "disablemusic": False,
"quickswap": False, "quickswap": False,
"heartcolor": "red", "heartcolor": "red",
"heartbeep": "normal", "heartbeep": "normal",
"sprite": None, "sprite": os.path.join(".","data","sprites","official","001.link.1.zspr"),
"fastmenu": "normal", "fastmenu": "normal",
"ow_palettes": "default", "ow_palettes": "default",
"uw_palettes": "default", "uw_palettes": "default",
# Spoiler defaults to FALSE
# Playthrough defaults to TRUE
# ROM defaults to TRUE
"create_spoiler": False, "create_spoiler": False,
"skip_playthrough": False, "calc_playthrough": True,
"suppress_rom": False, "create_rom": True,
"usestartinventory": False, "usestartinventory": False,
"custom": False, "custom": False,
"rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"), "rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"),
"seed": None, "seed": "",
"count": None, "count": 1,
"startinventory": "", "startinventory": "",
"beemizer": 0, "beemizer": 0,
"remote_items": False, "remote_items": False,
"race": False, "race": False,
"customitemarray": { "customitemarray": {
"bow": 0, "bow": 0,
"progressivebow": 2, "progressivebow": 2,
"boomerang": 1, "boomerang": 1,
"redmerang": 1, "redmerang": 1,
"hookshot": 1, "hookshot": 1,
"mushroom": 1, "mushroom": 1,
"powder": 1, "powder": 1,
"firerod": 1, "firerod": 1,
"icerod": 1, "icerod": 1,
"bombos": 1, "bombos": 1,
"ether": 1, "ether": 1,
"quake": 1, "quake": 1,
"lamp": 1, "lamp": 1,
"hammer": 1, "hammer": 1,
"shovel": 1, "shovel": 1,
"flute": 1, "flute": 1,
"bugnet": 1, "bugnet": 1,
"book": 1, "book": 1,
"bottle": 4, "bottle": 4,
"somaria": 1, "somaria": 1,
"byrna": 1, "byrna": 1,
"cape": 1, "cape": 1,
"mirror": 1, "mirror": 1,
"boots": 1, "boots": 1,
"powerglove": 0, "powerglove": 0,
"titansmitt": 0, "titansmitt": 0,
"progressiveglove": 2, "progressiveglove": 2,
"flippers": 1, "flippers": 1,
"pearl": 1, "pearl": 1,
"heartpiece": 24, "heartpiece": 24,
"heartcontainer": 10, "heartcontainer": 10,
"sancheart": 1, "sancheart": 1,
"sword1": 0, "sword1": 0,
"sword2": 0, "sword2": 0,
"sword3": 0, "sword3": 0,
"sword4": 0, "sword4": 0,
"progressivesword": 4, "progressivesword": 4,
"shield1": 0, "shield1": 0,
"shield2": 0, "shield2": 0,
"shield3": 0, "shield3": 0,
"progressiveshield": 3, "progressiveshield": 3,
"mail2": 0, "mail2": 0,
"mail3": 0, "mail3": 0,
"progressivemail": 2, "progressivemail": 2,
"halfmagic": 1, "halfmagic": 1,
"quartermagic": 0, "quartermagic": 0,
"bombsplus5": 0, "bombsplus5": 0,
"bombsplus10": 0, "bombsplus10": 0,
"arrowsplus5": 0, "arrowsplus5": 0,
"arrowsplus10": 0, "arrowsplus10": 0,
"arrow1": 1, "arrow1": 1,
"arrow10": 12, "arrow10": 12,
"bomb1": 0, "bomb1": 0,
"bomb3": 16, "bomb3": 16,
"bomb10": 1, "bomb10": 1,
"rupee1": 2, "rupee1": 2,
"rupee5": 4, "rupee5": 4,
"rupee20": 28, "rupee20": 28,
"rupee50": 7, "rupee50": 7,
"rupee100": 1, "rupee100": 1,
"rupee300": 5, "rupee300": 5,
"blueclock": 0, "blueclock": 0,
"greenclock": 0, "greenclock": 0,
"redclock": 0, "redclock": 0,
"silversupgrade": 0, "silversupgrade": 0,
"generickeys": 0, "generickeys": 0,
"triforcepieces": 0, "triforcepieces": 0,
"triforcepiecesgoal": 0, "triforcepiecesgoal": 0,
"triforce": 0, "triforce": 0,
"rupoor": 0, "rupoor": 0,
"rupoorcost": 10 "rupoorcost": 10
}, },
"randomSprite": False, "randomSprite": False,
"outputpath": os.path.join("."), "outputpath": os.path.join("."),
"saveonexit": "ask", "saveonexit": "ask",
"outputname": "",
"startinventoryarray": {} "startinventoryarray": {}
} }
@@ -477,11 +267,14 @@ def get_settings():
settings[k] = v settings[k] = v
return settings return settings
# Priority fallback is:
# 1: CLI
# 2: Settings file
# 3: Canned defaults
def get_args_priority(settings_args, gui_args, cli_args): def get_args_priority(settings_args, gui_args, cli_args):
args = {} args = {}
args["settings"] = get_settings() if settings_args is None else settings_args args["settings"] = parse_settings() if settings_args is None else settings_args
args["gui"] = {} if gui_args is None else gui_args args["gui"] = gui_args
args["cli"] = cli_args args["cli"] = cli_args
args["load"] = args["settings"] args["load"] = args["settings"]
@@ -492,17 +285,38 @@ def get_args_priority(settings_args, gui_args, cli_args):
if args["cli"] is None: if args["cli"] is None:
args["cli"] = {} args["cli"] = {}
cli = vars(parse_arguments(None)) cli = vars(parse_cli(None))
for k, v in cli.items(): for k, v in cli.items():
if isinstance(v, dict) and 1 in v: if isinstance(v, dict) and 1 in v:
args["cli"][k] = v[1] args["cli"][k] = v[1]
else: else:
args["cli"][k] = v args["cli"][k] = v
load_doesnt_have_key = k not in args["load"] args["cli"] = argparse.Namespace(**args["cli"])
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 cli = vars(args["cli"])
if load_doesnt_have_key or different_val: for k in vars(args["cli"]):
if not cli_has_empty_dict: load_doesnt_have_key = k not in args["load"]
args["load"][k] = args["cli"][k] 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]
else:
newArgs[key] = args[key]
newArgs[key] = update_deprecated_args(newArgs[key])
args = newArgs
return args return args
+38 -28
View File
@@ -318,7 +318,7 @@ def within_dungeon(world, player):
dungeon_builders[key] = simple_dungeon_builder(key, sector_list) dungeon_builders[key] = simple_dungeon_builder(key, sector_list)
dungeon_builders[key].entrance_list = list(entrances_map[key]) dungeon_builders[key].entrance_list = list(entrances_map[key])
recombinant_builders = {} recombinant_builders = {}
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, world.fish)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
paths = determine_required_paths(world, player) paths = determine_required_paths(world, player)
@@ -328,15 +328,15 @@ def within_dungeon(world, player):
start = time.process_time() start = time.process_time()
for builder in world.dungeon_layouts[player].values(): for builder in world.dungeon_layouts[player].values():
shuffle_key_doors(builder, world, player) shuffle_key_doors(builder, world, player)
logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start) logging.getLogger('').info('%s: %s', world.fish.translate("cli","cli","keydoor.shuffle.time"), time.process_time()-start)
smooth_door_pairs(world, player) smooth_door_pairs(world, player)
def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map): def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, fish):
for name, split_list in split_region_starts.items(): for name, split_list in split_region_starts.items():
builder = dungeon_builders.pop(name) builder = dungeon_builders.pop(name)
recombinant_builders[name] = builder recombinant_builders[name] = builder
split_builders = split_dungeon_builder(builder, split_list) split_builders = split_dungeon_builder(builder, split_list, fish)
dungeon_builders.update(split_builders) dungeon_builders.update(split_builders)
for sub_name, split_entrances in split_list.items(): for sub_name, split_entrances in split_list.items():
sub_builder = dungeon_builders[name+' '+sub_name] sub_builder = dungeon_builders[name+' '+sub_name]
@@ -370,7 +370,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
last_key = builder.name last_key = builder.name
loops += 1 loops += 1
else: else:
logging.getLogger('').info('Generating dungeon: %s', builder.name) logging.getLogger('').info('%s: %s', world.fish.translate("cli","cli","generating.dungeon"), builder.name)
ds = generate_dungeon(builder, origin_list_sans_drops, split_dungeon, world, player) ds = generate_dungeon(builder, origin_list_sans_drops, split_dungeon, world, player)
find_new_entrances(ds, entrances_map, connections, potentials, enabled_entrances, world, player) find_new_entrances(ds, entrances_map, connections, potentials, enabled_entrances, world, player)
ds.name = name ds.name = name
@@ -521,7 +521,7 @@ def shuffle_dungeon(world, player, start_region_names, dungeon_region_names):
for door in get_doors(world, world.get_region(name, player), player): for door in get_doors(world, world.get_region(name, player), player):
ugly_regions[door.name] = 0 ugly_regions[door.name] = 0
available_doors.append(door) available_doors.append(door)
# Loop until all available doors are used # Loop until all available doors are used
while len(available_doors) > 0: while len(available_doors) > 0:
# Pick a random available door to connect, prioritizing ones that aren't blocked. # Pick a random available door to connect, prioritizing ones that aren't blocked.
@@ -691,7 +691,7 @@ def cross_dungeon(world, player):
key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name] key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name]
loc.forced_item = loc.item = ItemFactory(key_name, player) loc.forced_item = loc.item = ItemFactory(key_name, player)
recombinant_builders = {} recombinant_builders = {}
handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map) handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, world.fish)
main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player)
@@ -699,26 +699,16 @@ def cross_dungeon(world, player):
check_required_paths(paths, world, player) check_required_paths(paths, world, player)
hc = world.get_dungeon('Hyrule Castle', player) hc = world.get_dungeon('Hyrule Castle', player)
del hc.dungeon_items[0] # removes map
hc.dungeon_items.append(ItemFactory('Compass (Escape)', player)) hc.dungeon_items.append(ItemFactory('Compass (Escape)', player))
at = world.get_dungeon('Agahnims Tower', player) at = world.get_dungeon('Agahnims Tower', player)
at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player)) at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player))
gt = world.get_dungeon('Ganons Tower', player) at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player))
del gt.dungeon_items[0] # removes map
assign_cross_keys(dungeon_builders, world, player) assign_cross_keys(dungeon_builders, world, player)
all_dungeon_items = [y for x in world.dungeons if x.player == player for y in x.all_items] all_dungeon_items = [y for x in world.dungeons if x.player == player for y in x.all_items]
target_items = 34 if world.retro[player] else 63 target_items = 34 if world.retro[player] else 63
d_items = target_items - len(all_dungeon_items) d_items = target_items - len(all_dungeon_items)
if d_items > 0: world.pool_adjustment[player] = d_items
if d_items >= 1: # restore HC map
world.get_dungeon('Hyrule Castle', player).dungeon_items.append(ItemFactory('Map (Escape)', player))
if d_items >= 2: # restore GT map
world.get_dungeon('Ganons Tower', player).dungeon_items.append(ItemFactory('Map (Ganons Tower)', player))
if d_items > 2:
world.pool_adjustment[player] = d_items - 2
elif d_items < 0:
world.pool_adjustment[player] = d_items
smooth_door_pairs(world, player) smooth_door_pairs(world, player)
# Re-assign dungeon bosses # Re-assign dungeon bosses
@@ -812,7 +802,7 @@ def assign_cross_keys(dungeon_builders, world, player):
dungeon.small_keys = [] dungeon.small_keys = []
else: else:
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
logging.getLogger('').info('Cross Dungeon: Key door shuffle time: %s', time.process_time()-start) logging.getLogger('').info('%s: %s', world.fish.translate("cli","cli","keydoor.shuffle.time.crossed"), time.process_time()-start)
def reassign_boss(boss_region, boss_key, builder, gt, world, player): def reassign_boss(boss_region, boss_key, builder, gt, world, player):
@@ -969,14 +959,14 @@ def calc_used_dungeon_items(builder):
def find_valid_combination(builder, start_regions, world, player, drop_keys=True): def find_valid_combination(builder, start_regions, world, player, drop_keys=True):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Shuffling Key doors for %s', builder.name) logger.info('%s %s', world.fish.translate("cli","cli","shuffling.keydoors"), builder.name)
# find valid combination of candidates # find valid combination of candidates
if len(builder.candidates) < builder.key_doors_num: if len(builder.candidates) < builder.key_doors_num:
if not drop_keys: if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False return False
builder.key_doors_num = len(builder.candidates) # reduce number of key doors builder.key_doors_num = len(builder.candidates) # reduce number of key doors
logger.info('Lowering key door count because not enough candidates: %s', builder.name) logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.candidates"), builder.name)
combinations = ncr(len(builder.candidates), builder.key_doors_num) combinations = ncr(len(builder.candidates), builder.key_doors_num)
itr = 0 itr = 0
start = time.process_time() start = time.process_time()
@@ -996,7 +986,7 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
if not drop_keys: if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num) logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False return False
logger.info('Lowering key door count because no valid layouts: %s', builder.name) logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), builder.name)
builder.key_doors_num -= 1 builder.key_doors_num -= 1
if builder.key_doors_num < 0: if builder.key_doors_num < 0:
raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name) raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name)
@@ -1043,8 +1033,9 @@ def log_key_logic(d_name, key_logic):
logger.debug('*Rule for %s:', rule.door_reference) logger.debug('*Rule for %s:', rule.door_reference)
if rule.bk_conditional_set: if rule.bk_conditional_set:
logger.debug('**BK Checks %s', ','.join([x.name for x in rule.bk_conditional_set])) logger.debug('**BK Checks %s', ','.join([x.name for x in rule.bk_conditional_set]))
logger.debug('**BK Blocked By Door (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk])) logger.debug('**BK Blocked (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk]))
logger.debug('**BK Elsewhere (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk])) if rule.needed_keys_w_bk:
logger.debug('**BK Available (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk]))
def build_pair_list(flat_list): def build_pair_list(flat_list):
@@ -1294,7 +1285,7 @@ def stateful_door(door, kind):
def random_door_type(door, partner, world, player, type_a, type_b, room_a, room_b): def random_door_type(door, partner, world, player, type_a, type_b, room_a, room_b):
r_kind = random.choices([DoorKind.Normal, DoorKind.Bombable, DoorKind.Dashable], [5, 2, 3], k=1)[0] r_kind = random.choices([DoorKind.Normal, DoorKind.Bombable, DoorKind.Dashable], [15, 4, 6], k=1)[0]
if r_kind != DoorKind.Normal: if r_kind != DoorKind.Normal:
if door.type == DoorType.Normal: if door.type == DoorType.Normal:
add_pair(door, partner, world, player) add_pair(door, partner, world, player)
@@ -1486,6 +1477,7 @@ class DROptions(Flag):
NoOptions = 0x00 NoOptions = 0x00
Eternal_Mini_Bosses = 0x01 # If on, GT minibosses marked as defeated when they try to spawn a heart Eternal_Mini_Bosses = 0x01 # If on, GT minibosses marked as defeated when they try to spawn a heart
Town_Portal = 0x02 # If on, Players will start with mirror scroll Town_Portal = 0x02 # If on, Players will start with mirror scroll
Map_Info = 0x04
Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required
# DATA GOES DOWN HERE # DATA GOES DOWN HERE
@@ -1572,10 +1564,12 @@ logical_connections = [
('Ice Big Chest Landing Push Blocks', 'Ice Big Chest View'), ('Ice Big Chest Landing Push Blocks', 'Ice Big Chest View'),
('Mire Lobby Gap', 'Mire Post-Gap'), ('Mire Lobby Gap', 'Mire Post-Gap'),
('Mire Post-Gap Gap', 'Mire Lobby'), ('Mire Post-Gap Gap', 'Mire Lobby'),
('Mire Hub Upper Blue Barrier', 'Mire Hub Top'), ('Mire Hub Upper Blue Barrier', 'Mire Hub Switch'),
('Mire Hub Lower Blue Barrier', 'Mire Hub Right'), ('Mire Hub Lower Blue Barrier', 'Mire Hub Right'),
('Mire Hub Right Blue Barrier', 'Mire Hub'), ('Mire Hub Right Blue Barrier', 'Mire Hub'),
('Mire Hub Top Blue Barrier', 'Mire Hub'), ('Mire Hub Top Blue Barrier', 'Mire Hub Switch'),
('Mire Hub Switch Blue Barrier N', 'Mire Hub Top'),
('Mire Hub Switch Blue Barrier S', 'Mire Hub'),
('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'), ('Mire Map Spike Side Drop Down', 'Mire Lone Shooter'),
('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spike Side Blue Barrier', 'Mire Crystal Dead End'),
('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'), ('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'),
@@ -2246,3 +2240,19 @@ compass_data = {
'Turtle Rock': (0x11F, 0xcb, 0x15e, 0, 0x18), 'Turtle Rock': (0x11F, 0xcb, 0x15e, 0, 0x18),
'Ganons Tower': (0x13A, 0xcc, 0x170, 2, 0x1a) 'Ganons Tower': (0x13A, 0xcc, 0x170, 2, 0x1a)
} }
# For compass boss indicator
boss_indicator = {
'Eastern Palace': (0x04, 'Eastern Boss SE'),
'Desert Palace': (0x06, 'Desert Boss SW'),
'Agahnims Tower': (0x08, 'Tower Agahnim 1 SW'),
'Swamp Palace': (0x0a, 'Swamp Boss SW'),
'Palace of Darkness': (0x0c, 'PoD Boss SE'),
'Misery Mire': (0x0e, 'Mire Boss SW'),
'Skull Woods': (0x10, 'Skull Spike Corner SW'),
'Ice Palace': (0x12, 'Ice Antechamber NE'),
'Tower of Hera': (0x14, 'Hera Boss Down Stairs'),
'Thieves Town': (0x16, 'Thieves Boss SE'),
'Turtle Rock': (0x18, 'TR Boss SW'),
'Ganons Tower': (0x1a, 'GT Agahnim 2 SW')
}
+6 -2
View File
@@ -666,8 +666,8 @@ def create_doors(world, player):
create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot
create_door(player, 'Ice Freezors Ledge Hole', Hole), create_door(player, 'Ice Freezors Ledge Hole', Hole),
create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2), create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2),
create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(2), create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1),
create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(1), create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(2),
create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0), create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0),
create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0).kill(), create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0).kill(),
create_door(player, 'Ice Hookshot Ledge Path', Lgcl), create_door(player, 'Ice Hookshot Ledge Path', Lgcl),
@@ -722,6 +722,8 @@ def create_doors(world, player):
create_door(player, 'Mire Hub Lower Blue Barrier', Lgcl), create_door(player, 'Mire Hub Lower Blue Barrier', Lgcl),
create_door(player, 'Mire Hub Right Blue Barrier', Lgcl), create_door(player, 'Mire Hub Right Blue Barrier', Lgcl),
create_door(player, 'Mire Hub Top Blue Barrier', Lgcl), create_door(player, 'Mire Hub Top Blue Barrier', Lgcl),
create_door(player, 'Mire Hub Switch Blue Barrier N', Lgcl),
create_door(player, 'Mire Hub Switch Blue Barrier S', Lgcl),
create_door(player, 'Mire Hub Right EN', Nrml).dir(Ea, 0xc2, Top, High).small_key().pos(0), create_door(player, 'Mire Hub Right EN', Nrml).dir(Ea, 0xc2, Top, High).small_key().pos(0),
create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2), create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2),
create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6), create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6),
@@ -1161,6 +1163,8 @@ def create_doors(world, player):
world.get_door('Mire Hub Lower Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Lower Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Right Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Right Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Top Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Hub Top Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Switch Blue Barrier N', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Hub Switch Blue Barrier S', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Map Spike Side Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Map Spike Side Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Map Spot Blue Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Map Spot Blue Barrier', player).barrier(CrystalBarrier.Blue)
world.get_door('Mire Crystal Dead End Left Barrier', player).barrier(CrystalBarrier.Blue) world.get_door('Mire Crystal Dead End Left Barrier', player).barrier(CrystalBarrier.Blue)
+15 -27
View File
@@ -710,7 +710,7 @@ class ExplorationState(object):
self.key_locations += 1 self.key_locations += 1
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
self.ttl_locations += 1 self.ttl_locations += 1
if location not in self.found_locations: if location not in self.found_locations: # todo: special logic for TT Boss?
self.found_locations.append(location) self.found_locations.append(location)
if not bk_Flag: if not bk_Flag:
self.bk_found.add(location) self.bk_found.add(location)
@@ -1152,8 +1152,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
# polarity: # polarity:
if not global_pole.is_valid(dungeon_map): if not global_pole.is_valid(dungeon_map):
raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!') raise NeutralizingException('Either free location/crystal assignment is already globally invalid - lazy dev check this earlier!')
logger.info('-Balancing Doors') logger.info(world.fish.translate("cli","cli","balance.doors"))
assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, world.fish)
# the rest # the rest
assign_the_rest(dungeon_map, neutral_sectors, global_pole) assign_the_rest(dungeon_map, neutral_sectors, global_pole)
return dungeon_map return dungeon_map
@@ -1322,7 +1322,7 @@ def assign_location_sectors(dungeon_map, free_location_sectors, global_pole):
totals[choice] += sector.chest_locations totals[choice] += sector.chest_locations
valid = True valid = True
for d_name, idx in d_idx.items(): for d_name, idx in d_idx.items():
if totals[idx] < minimal_locations(d_name): if totals[idx] < 5: # min locations for dungeons is 5 (bk exception)
valid = False valid = False
break break
for i, choice in enumerate(choices): for i, choice in enumerate(choices):
@@ -1353,18 +1353,6 @@ def weighted_random_locations(dungeon_map, free_location_sectors):
return choices, d_idx, totals return choices, d_idx, totals
def minimal_locations(dungeon_name):
# bump to 5 if maps do something useful for all these dungeons
if dungeon_name == 'Hyrule Castle':
return 4 # bk + compass + 2 others
if dungeon_name == 'Agahnims Tower':
return 4
if dungeon_name == 'Ganons Tower':
return 4
# reduce gt to 4 once compasses work
return 5
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False):
population = [] population = []
some_c_switches_present = False some_c_switches_present = False
@@ -1566,9 +1554,9 @@ def sum_polarity(sector_list):
return pol return pol
def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger): def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, fish):
# step 1: fix polarity connection issues # step 1: fix polarity connection issues
logger.info('--Basic Traversal') logger.info(fish.translate("cli","cli","basic.traversal"))
unconnected_builders = identify_polarity_issues(dungeon_map) unconnected_builders = identify_polarity_issues(dungeon_map)
while len(unconnected_builders) > 0: while len(unconnected_builders) > 0:
for name, builder in unconnected_builders.items(): for name, builder in unconnected_builders.items():
@@ -1606,7 +1594,7 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
problem_builders = identify_simple_branching_issues(problem_builders) problem_builders = identify_simple_branching_issues(problem_builders)
# step 3: fix neutrality issues # step 3: fix neutrality issues
polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger) polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger, fish)
# step 4: fix dead ends again # step 4: fix dead ends again
neutral_choices: List[List] = neutralize_the_rest(polarized_sectors) neutral_choices: List[List] = neutralize_the_rest(polarized_sectors)
@@ -1657,7 +1645,7 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
tries += 1 tries += 1
def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger): def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger, fish):
# step 3a: fix odd builders # step 3a: fix odd builders
odd_builders = [x for x in dungeon_map.values() if sum_polarity(x.sectors).charge() % 2 != 0] odd_builders = [x for x in dungeon_map.values() if sum_polarity(x.sectors).charge() % 2 != 0]
random.shuffle(odd_builders) random.shuffle(odd_builders)
@@ -1688,7 +1676,7 @@ def polarity_step_3(dungeon_map, polarized_sectors, global_pole, logger):
random.shuffle(builder_order) random.shuffle(builder_order)
for builder in builder_order: for builder in builder_order:
# global_pole.check_odd_polarities(polarized_sectors, dungeon_map) # global_pole.check_odd_polarities(polarized_sectors, dungeon_map)
logger.info('--Balancing %s', builder.name) logger.info('%s %s', fish.translate("cli", "cli", "balancing"), builder.name)
while not builder.polarity().is_neutral(): while not builder.polarity().is_neutral():
rejects = [] rejects = []
candidates = find_neutralizing_candidates(builder, polarized_sectors, rejects) candidates = find_neutralizing_candidates(builder, polarized_sectors, rejects)
@@ -2203,9 +2191,9 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole):
tries += 1 tries += 1
def split_dungeon_builder(builder, split_list): def split_dungeon_builder(builder, split_list, fish):
logger = logging.getLogger('') logger = logging.getLogger('')
logger.info('Splitting Up Desert/Skull') logger.info(fish.translate("cli","cli","splitting.up") + ' ' + 'Desert/Skull')
candidate_sectors = dict.fromkeys(builder.sectors) candidate_sectors = dict.fromkeys(builder.sectors)
global_pole = GlobalPolarity(candidate_sectors) global_pole = GlobalPolarity(candidate_sectors)
@@ -2216,10 +2204,10 @@ def split_dungeon_builder(builder, split_list):
sub_builder.all_entrances = split_entrances sub_builder.all_entrances = split_entrances
for r_name in split_entrances: for r_name in split_entrances:
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole)
return balance_split(candidate_sectors, dungeon_map, global_pole) return balance_split(candidate_sectors, dungeon_map, global_pole, fish)
def balance_split(candidate_sectors, dungeon_map, global_pole): def balance_split(candidate_sectors, dungeon_map, global_pole, fish):
logger = logging.getLogger('') logger = logging.getLogger('')
# categorize sectors # categorize sectors
check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole) check_for_forced_dead_ends(dungeon_map, candidate_sectors, global_pole)
@@ -2236,8 +2224,8 @@ def balance_split(candidate_sectors, dungeon_map, global_pole):
# blue barriers # blue barriers
assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole) assign_crystal_barrier_sectors(dungeon_map, crystal_barriers, global_pole)
# polarity: # polarity:
logger.info('-Re-balancing ' + next(iter(dungeon_map.keys())) + ' et al') logger.info(fish.translate("cli","cli","re-balancing") + ' ' + next(iter(dungeon_map.keys())) + ' et al')
assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger) assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger, fish)
# the rest # the rest
assign_the_rest(dungeon_map, neutral_sectors, global_pole) assign_the_rest(dungeon_map, neutral_sectors, global_pole)
return dungeon_map return dungeon_map
+26 -9
View File
@@ -8,14 +8,24 @@ import textwrap
import shlex import shlex
import sys import sys
from CLI import parse_arguments from source.classes.BabelFish import BabelFish
from Main import main import source.classes.diags as diagnostics
from CLI import parse_cli, get_args_priority
from Main import main, EnemizerError, __version__
from Rom import get_sprite_from_name from Rom import get_sprite_from_name
from Utils import is_bundled, close_console from Utils import is_bundled, close_console
from Fill import FillError from Fill import FillError
def start(): def start():
args = parse_arguments(None) args = parse_cli(None)
# print diagnostics
# usage: py DungeonRandomizer.py --diags
if args.diags:
diags = diagnostics.output(__version__)
print("\n".join(diags))
sys.exit(0)
if is_bundled() and len(sys.argv) == 1: if is_bundled() and len(sys.argv) == 1:
# for the bundled builds, if we have no arguments, the user # for the bundled builds, if we have no arguments, the user
@@ -42,20 +52,27 @@ def start():
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel] loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel) logging.basicConfig(format='%(message)s', level=loglevel)
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: if args.gui:
from Gui import guiMain from Gui import guiMain
guiMain(args) 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) seed = args.seed or random.randint(0, 999999999)
failures = [] failures = []
logger = logging.getLogger('') logger = logging.getLogger('')
for _ in range(args.count): for _ in range(args.count):
try: try:
main(seed=seed, args=args) main(seed=seed, args=args, fish=fish)
logger.info('Finished run %s', _+1) logger.info('%s %s', fish.translate("cli","cli","finished.run"), _+1)
except (FillError, Exception, RuntimeError) as err: except (FillError, EnemizerError, Exception, RuntimeError) as err:
failures.append((err, seed)) failures.append((err, seed))
logger.warning('Generation failed: %s', err) logger.warning('%s: %s', fish.translate("cli","cli","generation.failed"), err)
seed = random.randint(0, 999999999) seed = random.randint(0, 999999999)
for fail in failures: for fail in failures:
logger.info('%s\tseed failed with: %s', fail[1], fail[0]) logger.info('%s\tseed failed with: %s', fail[1], fail[0])
@@ -66,7 +83,7 @@ def start():
logger.info('Generation fail rate: ' + str(fail_rate[0] ).rjust(3, " ") + '.' + str(fail_rate[1] ).ljust(6, '0') + '%') logger.info('Generation fail rate: ' + str(fail_rate[0] ).rjust(3, " ") + '.' + str(fail_rate[1] ).ljust(6, '0') + '%')
logger.info('Generation success rate: ' + str(success_rate[0]).rjust(3, " ") + '.' + str(success_rate[1]).ljust(6, '0') + '%') logger.info('Generation success rate: ' + str(success_rate[0]).rjust(3, " ") + '.' + str(success_rate[1]).ljust(6, '0') + '%')
else: else:
main(seed=args.seed, args=args) main(seed=args.seed, args=args, fish=fish)
if __name__ == '__main__': if __name__ == '__main__':
+11 -3
View File
@@ -1,8 +1,12 @@
# -*- mode: python -*- # -*- mode: python -*-
import sys
block_cipher = None block_cipher = None
console = True console = True
BINARY_SLUG = "DungeonRandomizer"
def recurse_for_py_files(names_so_far): def recurse_for_py_files(names_so_far):
returnvalue = [] returnvalue = []
for name in os.listdir(os.path.join(*names_so_far)): for name in os.listdir(os.path.join(*names_so_far)):
@@ -21,10 +25,14 @@ def recurse_for_py_files(names_so_far):
return returnvalue return returnvalue
hiddenimports = [] hiddenimports = []
binaries = []
a = Analysis(['DungeonRandomizer.py'], #if sys.platform.find("windows"):
# binaries.append(("ucrtbase.dll","."))
a = Analysis([f"./{BINARY_SLUG}.py"],
pathex=[], pathex=[],
binaries=[], binaries=binaries,
datas=[], datas=[],
hiddenimports=hiddenimports, hiddenimports=hiddenimports,
hookspath=[], hookspath=[],
@@ -50,7 +58,7 @@ exe = EXE(pyz,
a.zipfiles, a.zipfiles,
a.datas, a.datas,
[], [],
name='DungeonRandomizer', name=BINARY_SLUG,
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
+5 -5
View File
@@ -255,11 +255,11 @@ ice_regions = [
] ]
mire_regions = [ mire_regions = [
'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Lone Shooter', 'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch',
'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End', 'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot',
'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room', 'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge',
'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge', 'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge',
'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp', 'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp',
'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest', 'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest',
'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool',
'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees',
+2 -2
View File
@@ -234,7 +234,7 @@ def valid_key_placement(item, location, itempool, world):
return True return True
key_logic = world.key_logic[item.player][dungeon.name] key_logic = world.key_logic[item.player][dungeon.name]
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
return key_logic.check_placement(unplaced_keys) return key_logic.check_placement(unplaced_keys, location if item.bigkey else None)
else: else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player]) inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player])) or (item.bigkey and not world.bigkeyshuffle[item.player]))
@@ -392,7 +392,7 @@ def balance_multiworld_progression(world):
threshold = max(reachable_locations_count.values()) - 20 threshold = max(reachable_locations_count.values()) - 20
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold] 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_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy() balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy() balancing_reachables = reachable_locations_count.copy()
+34 -22
View File
@@ -4,26 +4,29 @@ import os
import sys import sys
from tkinter import Tk, Button, BOTTOM, TOP, StringVar, BooleanVar, X, BOTH, RIGHT, ttk, messagebox from tkinter import Tk, Button, BOTTOM, TOP, StringVar, BooleanVar, X, BOTH, RIGHT, ttk, messagebox
from argparse import Namespace from CLI import get_args_priority
from CLI import get_settings, get_args_priority from DungeonRandomizer import parse_cli
from DungeonRandomizer import parse_arguments from source.gui.adjust.overview import adjust_page
from gui.adjust.overview import adjust_page from source.gui.startinventory.overview import startinventory_page
from gui.startinventory.overview import startinventory_page from source.gui.custom.overview import custom_page
from gui.custom.overview import custom_page from source.gui.loadcliargs import loadcliargs, loadadjustargs
from gui.loadcliargs import loadcliargs, loadadjustargs from source.gui.randomize.item import item_page
from gui.randomize.item import item_page from source.gui.randomize.entrando import entrando_page
from gui.randomize.entrando import entrando_page from source.gui.randomize.enemizer import enemizer_page
from gui.randomize.enemizer import enemizer_page from source.gui.randomize.dungeon import dungeon_page
from gui.randomize.dungeon import dungeon_page #from source.gui.randomize.multiworld import multiworld_page
from gui.randomize.multiworld import multiworld_page from source.gui.randomize.gameoptions import gameoptions_page
from gui.randomize.gameoptions import gameoptions_page from source.gui.randomize.generation import generation_page
from gui.randomize.generation import generation_page from source.gui.bottom import bottom_frame, create_guiargs
from gui.bottom import bottom_frame, create_guiargs
from GuiUtils import set_icon from GuiUtils import set_icon
from Main import __version__ as ESVersion from Main import __version__ as ESVersion
from source.classes.BabelFish import BabelFish
from source.classes.Empty import Empty
def guiMain(args=None): def guiMain(args=None):
# Save settings to file
def save_settings(args): def save_settings(args):
user_resources_path = os.path.join(".", "resources", "user") user_resources_path = os.path.join(".", "resources", "user")
settings_path = os.path.join(user_resources_path) settings_path = os.path.join(user_resources_path)
@@ -35,6 +38,7 @@ def guiMain(args=None):
f.write(json.dumps(args, indent=2)) f.write(json.dumps(args, indent=2))
os.chmod(os.path.join(settings_path, "settings.json"),0o755) os.chmod(os.path.join(settings_path, "settings.json"),0o755)
# Save settings from GUI
def save_settings_from_gui(confirm): def save_settings_from_gui(confirm):
gui_args = vars(create_guiargs(self)) gui_args = vars(create_guiargs(self))
if self.randomSprite.get(): if self.randomSprite.get():
@@ -73,9 +77,13 @@ def guiMain(args=None):
# get args # get args
# getting Settings & CLI (no GUI built yet) # getting Settings & CLI (no GUI built yet)
self.args = get_args_priority(None, None, None) 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 # get saved settings
self.settings = self.args["settings"] self.settings = vars(self.args["settings"])
# make array for pages # make array for pages
self.pages = {} self.pages = {}
@@ -83,6 +91,7 @@ def guiMain(args=None):
# make array for frames # make array for frames
self.frames = {} self.frames = {}
# make pages for each section
self.notebook = ttk.Notebook(self) self.notebook = ttk.Notebook(self)
self.pages["randomizer"] = ttk.Frame(self.notebook) self.pages["randomizer"] = ttk.Frame(self.notebook)
self.pages["adjust"] = 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") self.pages["randomizer"].notebook.add(self.pages["randomizer"].pages["dungeon"], text="Dungeon Shuffle")
# Multiworld # Multiworld
self.pages["randomizer"].pages["multiworld"],self.settings = multiworld_page(self.pages["randomizer"].notebook,self.settings) # 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"].notebook.add(self.pages["randomizer"].pages["multiworld"], text="Multiworld")
# Game Options # Game Options
self.pages["randomizer"].pages["gameoptions"] = gameoptions_page(self, self.pages["randomizer"].notebook) 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() self.pages["randomizer"].notebook.pack()
# bottom of window: Open Output Directory, Open Documentation (if exists) # bottom of window: Open Output Directory, Open Documentation (if exists)
self.frames["bottom"] = bottom_frame(self, self, None) self.pages["bottom"] = Empty()
self.pages["bottom"].pages = {}
self.pages["bottom"].pages["content"] = bottom_frame(self, self, None)
## Save Settings Button ## Save Settings Button
savesettingsButton = Button(self.frames["bottom"], text='Save Settings to File', command=lambda: save_settings_from_gui(True)) savesettingsButton = Button(self.pages["bottom"].pages["content"], text='Save Settings to File', command=lambda: save_settings_from_gui(True))
savesettingsButton.pack(side=RIGHT) savesettingsButton.pack(side=RIGHT)
# set bottom frame to main window # set bottom frame to main window
self.frames["bottom"].pack(side=BOTTOM, fill=X, padx=5, pady=5) self.pages["bottom"].pages["content"].pack(side=BOTTOM, fill=X, padx=5, pady=5)
self.outputPath = StringVar() self.outputPath = StringVar()
self.randomSprite = BooleanVar() self.randomSprite = BooleanVar()
@@ -178,9 +189,10 @@ def guiMain(args=None):
# load adjust settings into options # load adjust settings into options
loadadjustargs(self, self.settings) loadadjustargs(self, self.settings)
# run main window
mainWindow.mainloop() mainWindow.mainloop()
if __name__ == '__main__': if __name__ == '__main__':
args = parse_arguments(None) args = parse_cli(None)
guiMain(args) guiMain(args)
+13 -3
View File
@@ -1,8 +1,14 @@
# -*- mode: python -*- # -*- mode: python -*-
import sys
block_cipher = None block_cipher = None
console = True console = True
if sys.platform.find("mac") or sys.platform.find("osx"):
console = False
BINARY_SLUG = "Gui"
def recurse_for_py_files(names_so_far): def recurse_for_py_files(names_so_far):
returnvalue = [] returnvalue = []
for name in os.listdir(os.path.join(*names_so_far)): for name in os.listdir(os.path.join(*names_so_far)):
@@ -21,10 +27,14 @@ def recurse_for_py_files(names_so_far):
return returnvalue return returnvalue
hiddenimports = [] hiddenimports = []
binaries = []
a = Analysis(['Gui.py'], #if sys.platform.find("windows"):
# binaries.append(("ucrtbase.dll","."))
a = Analysis([f"./{BINARY_SLUG}.py"],
pathex=[], pathex=[],
binaries=[], binaries=binaries,
datas=[], datas=[],
hiddenimports=hiddenimports, hiddenimports=hiddenimports,
hookspath=[], hookspath=[],
@@ -50,7 +60,7 @@ exe = EXE(pyz,
a.zipfiles, a.zipfiles,
a.datas, a.datas,
[], [],
name='Gui', name=BINARY_SLUG,
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
+4 -3
View File
@@ -1,13 +1,14 @@
import queue import queue
import os
import threading import threading
import tkinter as tk import tkinter as tk
from Utils import local_path from Utils import local_path
def set_icon(window): def set_icon(window):
er16 = tk.PhotoImage(file=local_path('data/ER16.gif')) er16 = tk.PhotoImage(file=local_path(os.path.join("data","ER16.gif")))
er32 = tk.PhotoImage(file=local_path('data/ER32.gif')) er32 = tk.PhotoImage(file=local_path(os.path.join("data","ER32.gif")))
er48 = tk.PhotoImage(file=local_path('data/ER32.gif')) er48 = tk.PhotoImage(file=local_path(os.path.join("data","ER48.gif")))
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
# Although tkinter is intended to be thread safe, there are many reports of issues # Although tkinter is intended to be thread safe, there are many reports of issues
+24 -20
View File
@@ -9,7 +9,7 @@ from EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive from Fill import FillError, fill_restrictive
from Items import ItemFactory 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. #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): def get_custom_array_key(item):
label_switcher = { label_switcher = {
"silverarrow": "silversupgrade", "silverarrow": "silversupgrade",
@@ -257,17 +258,17 @@ def generate_itempool(world, player):
# set up item pool # set up item pool
if world.custom: 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) (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: 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]) (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(): if player in world.pool_adjustment.keys():
amt = world.pool_adjustment[player] amt = world.pool_adjustment[player]
if amt < 0: if amt < 0:
for i in range(0, amt): for _ in range(0, amt):
pool.remove('Rupees (20)') pool.remove('Rupees (20)')
elif amt > 0: elif amt > 0:
for i in range(0, amt): for _ in range(0, amt):
pool.append('Rupees (20)') pool.append('Rupees (20)')
for item in precollected_items: 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) # 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) # 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) # 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 [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] adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
for hp in adv_heart_pieces: for hp in adv_heart_pieces:
hp.advancement = True 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) 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): 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 = [] pool = []
placed_items = {} placed_items = {}
precollected_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 itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place: if itemtotal < total_items_to_place:
nothings = total_items_to_place - itemtotal nothings = total_items_to_place - itemtotal
print("Placing " + str(nothings) + " Nothings") # print("Placing " + str(nothings) + " Nothings")
pool.extend(['Nothing'] * nothings) pool.extend(['Nothing'] * nothings)
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) 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 difficulty in ['normal', 'hard', 'expert']:
for goal in ['ganon', 'triforcehunt', 'pedestal']: for goal in ['ganon', 'triforcehunt', 'pedestal']:
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']: 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 swords in ['random', 'assured', 'swordless', 'vanilla']:
for progressive in ['on', 'off']: for progressive in ['on', 'off']:
for shuffle in ['full', 'insanity_legacy']: for shuffle in ['full', 'insanity_legacy']:
for retro in [True, False]: for retro in [True, False]:
out = get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro) for door_shuffle in ['basic', 'crossed', 'vanilla']:
count = len(out[0]) + len(out[1]) 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 correct_count = total_items_to_place
if goal == 'pedestal' and swords != 'vanilla': if goal == 'pedestal' and swords != 'vanilla':
# pedestal goals generate one extra item # pedestal goals generate one extra item
correct_count += 1 correct_count += 1
if retro: if retro:
correct_count += 28 correct_count += 28
try: 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)) 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: except AssertionError as e:
print(e) print(e)
if __name__ == '__main__': if __name__ == '__main__':
test() test()
+1
View File
@@ -130,6 +130,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'), 'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'), 'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'), 'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), 'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), 'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
+261 -87
View File
@@ -1,3 +1,4 @@
import itertools
import logging import logging
from collections import defaultdict, deque from collections import defaultdict, deque
@@ -22,6 +23,7 @@ class KeyLayout(object):
self.all_chest_locations = {} self.all_chest_locations = {}
self.big_key_special = False self.big_key_special = False
self.all_locations = set() self.all_locations = set()
self.item_locations = set()
# bk special? # bk special?
# bk required? True if big chests or big doors exists # bk required? True if big chests or big doors exists
@@ -31,6 +33,8 @@ class KeyLayout(object):
self.flat_prop = flatten_pair_list(self.proposal) self.flat_prop = flatten_pair_list(self.proposal)
self.key_logic = KeyLogic(self.sector.name) self.key_logic = KeyLogic(self.sector.name)
self.max_chests = calc_max_chests(builder, self, world, player) self.max_chests = calc_max_chests(builder, self, world, player)
self.all_locations = set()
self.item_locations = set()
class KeyLogic(object): class KeyLogic(object):
@@ -48,10 +52,14 @@ class KeyLogic(object):
self.placement_rules = [] self.placement_rules = []
self.outside_keys = 0 self.outside_keys = 0
def check_placement(self, unplaced_keys): def check_placement(self, unplaced_keys, big_key_loc=None):
for rule in self.placement_rules: for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, unplaced_keys): if not rule.is_satisfiable(self.outside_keys, unplaced_keys):
return False return False
if big_key_loc:
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
if rule_a.contradicts(rule_b, unplaced_keys, big_key_loc):
return False
return True return True
@@ -66,6 +74,7 @@ class DoorRules(object):
# for a place with only 1 free location/key_only_location behind it ... no goals and locations # for a place with only 1 free location/key_only_location behind it ... no goals and locations
self.allow_small = False self.allow_small = False
self.small_location = None self.small_location = None
self.opposite = None
class PlacementRule(object): class PlacementRule(object):
@@ -78,6 +87,36 @@ class PlacementRule(object):
self.needed_keys_wo_bk = None self.needed_keys_wo_bk = None
self.check_locations_w_bk = None self.check_locations_w_bk = None
self.check_locations_wo_bk = None self.check_locations_wo_bk = None
self.bk_relevant = True
def contradicts(self, rule, unplaced_keys, big_key_loc):
bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
rule_blocked = big_key_loc in rule.bk_conditional_set if rule.bk_conditional_set else False
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
if check_locations is None or rule_locations is None:
return False
check_locations = check_locations - {big_key_loc}
rule_locations = rule_locations - {big_key_loc}
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
rule_threshold = rule.needed_keys_wo_bk if rule_blocked else rule.needed_keys_w_bk
common_locations = rule_locations & check_locations
shared = len(common_locations)
if min(rule_threshold, threshold) - shared > 0:
left = unplaced_keys - shared
check_locations = check_locations - common_locations
check_needed = threshold - shared
if len(check_locations) < check_needed or left < check_needed:
return True
else:
left -= check_needed
rule_locations = rule_locations - common_locations
rule_needed = rule_threshold - shared
if len(rule_locations) < rule_needed or left < rule_needed:
return True
else:
left -= rule_needed
return False
def is_satisfiable(self, outside_keys, unplaced_keys): def is_satisfiable(self, outside_keys, unplaced_keys):
bk_blocked = False bk_blocked = False
@@ -86,9 +125,11 @@ class PlacementRule(object):
if loc.item and loc.item.bigkey: if loc.item and loc.item.bigkey:
bk_blocked = True bk_blocked = True
break break
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
if not bk_blocked and check_locations is None:
return True
available_keys = outside_keys available_keys = outside_keys
empty_chests = 0 empty_chests = 0
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
for loc in check_locations: for loc in check_locations:
if not loc.item: if not loc.item:
@@ -134,9 +175,27 @@ def build_key_layout(builder, start_regions, proposal, world, player):
key_layout.max_drops = count_key_drops(key_layout.sector) key_layout.max_drops = count_key_drops(key_layout.sector)
key_layout.max_chests = calc_max_chests(builder, key_layout, world, player) key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set() key_layout.big_key_special = 'Hyrule Dungeon Cellblock' in key_layout.sector.region_set()
key_layout.all_locations = find_all_locations(key_layout.sector)
return key_layout return key_layout
def count_key_drops(sector):
cnt = 0
for region in sector.regions:
for loc in region.locations:
if loc.event and 'Small Key' in loc.item.name:
cnt += 1
return cnt
def find_all_locations(sector):
all_locations = set()
for region in sector.regions:
for loc in region.locations:
all_locations.add(loc)
return all_locations
def calc_max_chests(builder, key_layout, world, player): def calc_max_chests(builder, key_layout, world, player):
if world.doorShuffle[player] != 'crossed': if world.doorShuffle[player] != 'crossed':
return len(world.get_dungeon(key_layout.sector.name, player).small_keys) return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
@@ -165,17 +224,13 @@ def analyze_dungeon(key_layout, world, player):
raw_avail = chest_keys + len(key_counter.key_only_locations) raw_avail = chest_keys + len(key_counter.key_only_locations)
available = raw_avail - key_counter.used_keys available = raw_avail - key_counter.used_keys
possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop) possible_smalls = count_unique_small_doors(key_counter, key_layout.flat_prop)
avail_bigs = exist_relevant_big_doors(key_counter, key_layout) avail_bigs = exist_relevant_big_doors(key_counter, key_layout) or exist_big_chest(key_counter)
non_big_locs = count_locations_big_optional(key_counter.free_locations) non_big_locs = count_locations_big_optional(key_counter.free_locations)
if not key_counter.big_key_opened: if not key_counter.big_key_opened:
if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
# todo: detect forced subsequent keys - see keypuzzles
# try to relax the rules here? - smallest requirement that doesn't force a softlock # try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = deque() child_queue = deque()
smallest_rule = None
for child in key_counter.child_doors.keys(): for child in key_counter.child_doors.keys():
if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened: if not child.bigKey or not key_layout.big_key_special or key_counter.big_key_opened:
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player) odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
@@ -183,34 +238,20 @@ def analyze_dungeon(key_layout, world, player):
child_queue.append((child, odd_counter, empty_flag)) child_queue.append((child, odd_counter, empty_flag))
if child in doors_completed and child in key_logic.door_rules.keys(): if child in doors_completed and child in key_logic.door_rules.keys():
rule = key_logic.door_rules[child] rule = key_logic.door_rules[child]
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
while len(child_queue) > 0: while len(child_queue) > 0:
child, odd_counter, empty_flag = child_queue.popleft() child, odd_counter, empty_flag = child_queue.popleft()
if not child.bigKey and child not in doors_completed: if not child.bigKey and child not in doors_completed:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag) best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
rule = create_rule(best_counter, key_counter, key_layout, world, player) rule = create_rule(best_counter, key_counter, key_layout, world, player)
# todo: seems to be caused by best_counter not opening the big key door when that's logically required. Re-evaluate usage of this
# if not rule.is_valid:
# logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name)
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player) check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player) bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule key_logic.door_rules[child.name] = rule
create_placement_rule(key_layout, child, odd_counter, key_counter, world, player)
doors_completed.add(child) doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout) next_counter = find_next_counter(child, key_counter, key_layout)
ctr_id = cid(next_counter, key_layout) ctr_id = cid(next_counter, key_layout)
if ctr_id not in visited_cid: if ctr_id not in visited_cid:
queue.append((child, next_counter)) queue.append((child, next_counter))
visited_cid.add(ctr_id) visited_cid.add(ctr_id)
possible_smalls_collected = len(key_counter.key_only_locations) + non_big_locs
if not key_counter.big_key_opened:
if smallest_rule is not None and smallest_rule >= possible_smalls_collected and not avail_bigs:
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
if not key_counter.big_key_opened and big_chest_in_locations(key_counter.free_locations):
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
check_rules(original_key_counter, key_layout, world, player) check_rules(original_key_counter, key_layout, world, player)
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location # Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
@@ -219,59 +260,49 @@ def analyze_dungeon(key_layout, world, player):
max_counter = find_max_counter(key_layout) max_counter = find_max_counter(key_layout)
rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc) rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc)
rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num
create_exhaustive_placement_rules(key_layout, world, player)
set_paired_rules(key_logic, world, player)
def create_placement_rule(key_layout, door, odd_ctr, current_ctr, world, player): def create_exhaustive_placement_rules(key_layout, world, player):
key_logic = key_layout.key_logic key_logic = key_layout.key_logic
worst_ctr = find_worst_counter(door, odd_ctr, current_ctr, key_layout, False) max_ctr = find_max_counter(key_layout)
sm_num = worst_ctr.used_keys + 1 for code, key_counter in key_layout.key_counters.items():
accessible_loc = set() accessible_loc = set()
accessible_loc.update(worst_ctr.free_locations) accessible_loc.update(key_counter.free_locations)
accessible_loc.update(worst_ctr.key_only_locations) accessible_loc.update(key_counter.key_only_locations)
worst_ctr_wo_bk, post_ctr, alt_num = find_worst_counter_wo_bk(sm_num, accessible_loc, door, odd_ctr, current_ctr, key_layout) blocked_loc = key_layout.item_locations.difference(accessible_loc)
blocked_loc = key_layout.all_locations.difference(accessible_loc) valid_rule = True
# min_keys = max(count_unique_sm_doors(key_counter.child_doors), key_counter.used_keys + 1)
if len(blocked_loc) > 0: min_keys = key_counter.used_keys + 1
rule = PlacementRule() if len(blocked_loc) > 0 and len(key_counter.key_only_locations) < min_keys:
rule.door_reference = door rule = PlacementRule()
rule.small_key = key_logic.small_key_name rule.door_reference = code
rule.needed_keys_w_bk = sm_num rule.small_key = key_logic.small_key_name
placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player) if key_counter.big_key_opened or not big_key_progress(key_counter):
rule.check_locations_w_bk = accessible_loc rule.needed_keys_w_bk = min_keys
if worst_ctr_wo_bk: rule.bk_relevant = key_counter.big_key_opened
accessible_wo_bk, post_set = set(), set() if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
accessible_wo_bk.update(worst_ctr_wo_bk.free_locations) valid_rule = False # indicates that the big key cannot be in the accessible locations
accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations) key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
post_set.update(post_ctr.free_locations) else:
post_set.update(post_ctr.key_only_locations) placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
blocked_wo_bk = post_set.difference(accessible_wo_bk) rule.check_locations_w_bk = accessible_loc
if len(blocked_wo_bk) > 0: check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
rule.bk_conditional_set = blocked_wo_bk else:
rule.needed_keys_wo_bk = alt_num if big_key_progress(key_counter) and only_sm_doors(key_counter):
# can this self lock a key if bk not avail? I'm thinking no. create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
# placement_self_lock_adjustment(rule, key_layout, ???, worst_ctr_wo_bk, world, player) rule.bk_conditional_set = blocked_loc
rule.check_locations_wo_bk = accessible_wo_bk rule.needed_keys_wo_bk = min_keys
key_logic.placement_rules.append(rule) rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
if worst_ctr_wo_bk: if valid_rule:
check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num) key_logic.placement_rules.append(rule)
refine_placement_rules(key_layout, max_ctr)
def check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num): def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
avail_keys = len(worst_ctr_wo_bk.key_only_locations)
place_able_keys = min(key_layout.max_chests, len(worst_ctr_wo_bk.free_locations))
if avail_keys + place_able_keys < alt_num:
accessible_wo_bk, post_set = set(), set()
accessible_wo_bk.update(worst_ctr_wo_bk.free_locations)
accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations)
post_set.update(post_ctr.free_locations)
post_set.update(post_ctr.key_only_locations)
key_layout.key_logic.bk_restricted.update(post_set.difference(accessible_wo_bk))
def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player):
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations': if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
max_ctr = find_max_counter(key_layout) blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
blocked_others = set(max_ctr.other_locations).difference(set(worst_ctr.other_locations))
important_found = False important_found = False
for loc in blocked_others: for loc in blocked_others:
if important_location(loc, world, player): if important_location(loc, world, player):
@@ -281,13 +312,115 @@ def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, wor
rule.needed_keys_w_bk -= 1 rule.needed_keys_w_bk -= 1
def count_key_drops(sector): def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
cnt = 0 if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
for region in sector.regions: key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
for loc in region.locations: return True
if loc.event and 'Small Key' in loc.item.name: return False
cnt += 1
return cnt
def refine_placement_rules(key_layout, max_ctr):
key_logic = key_layout.key_logic
changed = True
while changed:
changed = False
rules_to_remove = []
for rule in key_logic.placement_rules:
if rule.check_locations_w_bk:
rule.check_locations_w_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_w_bk.intersection(max_ctr.key_only_locations)
if len(key_onlys) > 0:
rule.check_locations_w_bk.difference_update(key_onlys)
rule.needed_keys_w_bk -= len(key_onlys)
if rule.needed_keys_w_bk == 0:
rules_to_remove.append(rule)
if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1:
new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk
if len(new_restricted - key_logic.bk_restricted) > 0:
key_logic.bk_restricted.update(new_restricted) # bk must be in one of the check_locations
changed = True
if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk:
logging.getLogger('').warning('Invalid rule - what went wrong here??')
rules_to_remove.append(rule)
changed = True
if rule.bk_conditional_set is not None:
rule.bk_conditional_set.difference_update(key_logic.bk_restricted)
rule.bk_conditional_set.difference_update(max_ctr.key_only_locations)
if len(rule.bk_conditional_set) == 0:
rules_to_remove.append(rule)
if rule.check_locations_wo_bk:
rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations)
if len(key_onlys) > 0:
rule.check_locations_wo_bk.difference_update(key_onlys)
rule.needed_keys_wo_bk -= len(key_onlys)
if rule.needed_keys_wo_bk == 0:
rules_to_remove.append(rule)
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
if len(rule.bk_conditional_set) > 0:
key_logic.bk_restricted.update(rule.bk_conditional_set)
rules_to_remove.append(rule)
changed = True # impossible for bk to be here, I think
for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2):
if rule_b.bk_conditional_set and rule_a.check_locations_w_bk:
temp = rule_a
rule_a = rule_b
rule_b = temp
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed:
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
rules_to_remove.append(rule_a)
changed = True
break
equivalent_rules = []
for rule in key_logic.placement_rules:
for rule2 in key_logic.placement_rules:
if rule != rule2:
if rule.check_locations_w_bk and rule2.check_locations_w_bk:
if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk:
rules_to_remove.append(rule)
elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk:
rules_to_remove.append(rule)
elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk:
equivalent_rules.append((rule, rule2))
if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set:
if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk:
rules_to_remove.append(rule)
elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk:
rules_to_remove.append(rule)
elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk:
equivalent_rules.append((rule, rule2))
if len(rules_to_remove) > 0:
key_logic.placement_rules = [x for x in key_logic.placement_rules if x not in rules_to_remove]
equivalent_rules = [x for x in equivalent_rules if x[0] not in rules_to_remove and x[1] not in rules_to_remove]
if len(equivalent_rules) > 0:
removed_rules = {}
for r1, r2 in equivalent_rules:
if r1 in removed_rules.keys():
r1 = removed_rules[r1]
if r2 in removed_rules.keys():
r2 = removed_rules[r2]
if r1 != r2:
r1.door_reference += ','+r2.door_reference
key_logic.placement_rules.remove(r2)
removed_rules[r2] = r1
def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player):
key_logic = key_layout.key_logic
rule = PlacementRule()
rule.door_reference = code
rule.small_key = key_logic.small_key_name
rule.needed_keys_w_bk = min_keys
if key_counter.big_key_opened and rule.needed_keys_w_bk + 1 > len(accessible_loc):
# indicates that the big key cannot be in the accessible locations
key_logic.bk_restricted.update(accessible_loc.difference(max_ctr.key_only_locations))
else:
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
rule.check_locations_w_bk = accessible_loc
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
key_logic.placement_rules.append(rule)
def queue_sorter(queue_item): def queue_sorter(queue_item):
@@ -314,8 +447,8 @@ def find_bk_locked_sections(key_layout, world, player):
big_chest_allowed_big_key = world.accessibility[player] != 'locations' big_chest_allowed_big_key = world.accessibility[player] != 'locations'
for counter in key_counters.values(): for counter in key_counters.values():
key_layout.all_chest_locations.update(counter.free_locations) key_layout.all_chest_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.free_locations) key_layout.item_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.key_only_locations) key_layout.item_locations.update(counter.key_only_locations)
if counter.big_key_opened and counter.important_location: if counter.big_key_opened and counter.important_location:
big_chest_allowed_big_key = False big_chest_allowed_big_key = False
if not counter.big_key_opened: if not counter.big_key_opened:
@@ -710,6 +843,16 @@ def count_unique_sm_doors(doors):
return len(unique_d_set) return len(unique_d_set)
def big_key_progress(key_counter):
return not only_sm_doors(key_counter) or exist_big_chest(key_counter)
def only_sm_doors(key_counter):
for door in key_counter.child_doors:
if door.bigKey:
return False
return True
# doesn't count dest doors # doesn't count dest doors
def count_unique_small_doors(key_counter, proposal): def count_unique_small_doors(key_counter, proposal):
cnt = 0 cnt = 0
@@ -738,6 +881,13 @@ def exist_relevant_big_doors(key_counter, key_layout):
return False return False
def exist_big_chest(key_counter):
for loc in key_counter.free_locations:
if '- Big Chest' in loc.name:
return True
return False
def count_locations_big_optional(locations, bk=False): def count_locations_big_optional(locations, bk=False):
cnt = 0 cnt = 0
for loc in locations: for loc in locations:
@@ -989,6 +1139,13 @@ def reduce_rules(small_rules, collected, collected_alt):
rule.small_key_num = collected rule.small_key_num = collected
def set_paired_rules(key_logic, world, player):
for d_name, rule in key_logic.door_rules.items():
door = world.get_door(d_name, player)
if door.dest.name in key_logic.door_rules.keys():
rule.opposite = key_logic.door_rules[door.dest.name]
# Soft lock stuff # Soft lock stuff
def validate_key_layout(key_layout, world, player): def validate_key_layout(key_layout, world, player):
# retro is all good - except for hyrule castle in standard mode # retro is all good - except for hyrule castle in standard mode
@@ -1015,7 +1172,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
ttl_key_only = count_key_only_locations(state) ttl_key_only = count_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_key_only, state, world, player) available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_key_only, state, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
if invalid_self_locking_key(state, prev_state, prev_avail, world, player): if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
return False return False
# todo: allow more key shuffles - refine placement rules # todo: allow more key shuffles - refine placement rules
# if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0): # if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
@@ -1054,18 +1211,24 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
return True return True
def invalid_self_locking_key(state, prev_state, prev_avail, world, player): def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
if prev_state is None or state.used_smalls == prev_state.used_smalls: if prev_state is None or state.used_smalls == prev_state.used_smalls:
return False return False
new_locations = set(state.found_locations).difference(set(prev_state.found_locations)) new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors))
state_copy = state.copy()
while len(new_bk_doors) > 0:
for door in new_bk_doors:
open_a_door(door.door, state_copy, key_layout.flat_prop)
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
expand_key_state(state_copy, key_layout.flat_prop, world, player)
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
important_found = False important_found = False
for loc in new_locations: for loc in new_locations:
important_found |= important_location(loc, world, player) important_found |= important_location(loc, world, player)
if not important_found: if not important_found:
return False return False
new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors)) new_small_doors = set(state.small_doors).difference(set(prev_state.small_doors))
new_bk_doors = set(state.big_doors).difference(set(prev_state.big_doors)) if len(new_small_doors) > 0:
if len(new_small_doors) > 0 or len(new_bk_doors) > 0:
return False return False
return prev_avail - 1 == 0 return prev_avail - 1 == 0
@@ -1151,11 +1314,21 @@ def create_key_counter(state, key_layout, world, player):
return key_counter return key_counter
def important_location(loc, world, player): imp_locations = None
important_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
def imp_locations_factory(world, player):
global imp_locations
if imp_locations:
return imp_locations
imp_locations = ['Agahnim 1', 'Agahnim 2', 'Attic Cracked Floor', 'Suspicious Maiden']
if world.mode[player] == 'standard' or world.doorShuffle[player] == 'crossed': if world.mode[player] == 'standard' or world.doorShuffle[player] == 'crossed':
important_locations.append('Hyrule Dungeon Cellblock') imp_locations.append('Hyrule Dungeon Cellblock')
return '- Prize' in loc.name or loc.name in important_locations return imp_locations
def important_location(loc, world, player):
return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player)
def create_odd_key_counter(door, parent_counter, key_layout, world, player): def create_odd_key_counter(door, parent_counter, key_layout, world, player):
@@ -1428,7 +1601,7 @@ def validate_key_placement(key_layout, world, player):
max_counter = find_max_counter(key_layout) max_counter = find_max_counter(key_layout)
big_key_outside = dungeon.big_key not in (l.item for l in max_counter.free_locations) big_key_outside = dungeon.big_key not in (l.item for l in max_counter.free_locations)
for counter in key_layout.key_counters.values(): for code, counter in key_layout.key_counters.items():
if len(counter.child_doors) == 0: if len(counter.child_doors) == 0:
continue continue
big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside
@@ -1445,6 +1618,7 @@ def validate_key_placement(key_layout, world, player):
# missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations? # missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0): if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
logging.getLogger('').error("Keylock - can't open locations: ") logging.getLogger('').error("Keylock - can't open locations: ")
logging.getLogger('').error("code: " + code)
for i in missing_locations: for i in missing_locations:
logging.getLogger('').error(i) logging.getLogger('').error(i)
return False return False
+110 -42
View File
@@ -24,10 +24,14 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute
from ItemList import generate_itempool, difficulties, fill_prizes from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
__version__ = '0.0.h-dev' __version__ = '0.0.22.0u'
def main(args, seed=None): class EnemizerError(RuntimeError):
pass
def main(args, seed=None, fish=None):
if args.outputpath: if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True) os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath output_path.cached_path = args.outputpath
@@ -59,10 +63,15 @@ def main(args, seed=None):
world.beemizer = args.beemizer.copy() world.beemizer = args.beemizer.copy()
world.experimental = args.experimental.copy() world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
world.fish = fish
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
logger.info('ALttP Door Randomizer Version %s - Seed: %s\n', __version__, world.seed) logger.info(
world.fish.translate("cli","cli","app.title") + "\n",
__version__,
world.seed
)
parsed_names = parse_player_names(args.names, world.players, args.teams) parsed_names = parse_player_names(args.names, world.players, args.teams)
world.teams = len(parsed_names) world.teams = len(parsed_names)
@@ -77,7 +86,8 @@ def main(args, seed=None):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]] world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
if world.mode[player] == 'standard' and world.enemy_shuffle[player] != 'none': 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(',')): for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player) item = ItemFactory(tok.strip(), player)
@@ -94,7 +104,7 @@ def main(args, seed=None):
create_rooms(world, player) create_rooms(world, player)
create_dungeons(world, player) create_dungeons(world, player)
logger.info('Shuffling the World about.') logger.info(world.fish.translate("cli","cli","shuffling.world"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
@@ -102,7 +112,7 @@ def main(args, seed=None):
else: else:
link_inverted_entrances(world, player) link_inverted_entrances(world, player)
logger.info('Shuffling dungeons') logger.info(world.fish.translate("cli","cli","shuffling.dungeons"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
link_doors(world, player) link_doors(world, player)
@@ -110,21 +120,21 @@ def main(args, seed=None):
mark_light_world_regions(world, player) mark_light_world_regions(world, player)
else: else:
mark_dark_world_regions(world, player) mark_dark_world_regions(world, player)
logger.info('Generating Item Pool.') logger.info(world.fish.translate("cli","cli","generating.itempool"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
generate_itempool(world, player) generate_itempool(world, player)
logger.info('Calculating Access Rules.') logger.info(world.fish.translate("cli","cli","calc.access.rules"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
set_rules(world, player) set_rules(world, player)
logger.info('Placing Dungeon Prizes.') logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes"))
fill_prizes(world) fill_prizes(world)
logger.info('Placing Dungeon Items.') logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
shuffled_locations = None shuffled_locations = None
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) + if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
@@ -138,9 +148,17 @@ def main(args, seed=None):
for player in range(1, world.players+1): for player in range(1, world.players+1):
for key_layout in world.key_layout[player].values(): for key_layout in world.key_layout[player].values():
if not validate_key_placement(key_layout, world, player): if not validate_key_placement(key_layout, world, player):
raise RuntimeError("Keylock detected: %s (Player %d)" % (key_layout.sector.name, player)) raise RuntimeError(
"%s: %s (%s %d)" %
(
world.fish.translate("cli","cli","keylock.detected"),
key_layout.sector.name,
world.fish.translate("cli","cli","player"),
player
)
)
logger.info('Fill the world.') logger.info(world.fish.translate("cli","cli","fill.world"))
if args.algorithm == 'flood': if args.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items flood_items(world) # different algo, biased towards early game progress items
@@ -159,20 +177,20 @@ def main(args, seed=None):
distribute_items_restrictive(world, True) distribute_items_restrictive(world, True)
if world.players > 1: if world.players > 1:
logger.info('Balancing multiworld progression.') logger.info(world.fish.translate("cli","cli","balance.multiworld"))
balance_multiworld_progression(world) balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom # if we only check for beatable, we can do this sanity check first before creating the rom
if not world.can_beat_game(): if not world.can_beat_game():
raise RuntimeError('Cannot beat game. Something went terribly wrong here!') raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
logger.info('Patching ROM.')
outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed) outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed)
rom_names = [] rom_names = []
jsonout = {} jsonout = {}
enemized = False
if not args.suppress_rom: if not args.suppress_rom:
logger.info(world.fish.translate("cli","cli","patching.rom"))
for team in range(world.teams): for team in range(world.teams):
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit' sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
@@ -182,16 +200,21 @@ def main(args, seed=None):
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom) rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer and (args.enemizercli or not args.jsonout): 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): if os.path.exists(args.enemizercli):
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit) patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
enemized = True
if not args.jsonout: if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000) rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
else: else:
logging.warning("EnemizerCLI not found at:" + args.enemizercli) enemizerMsg = world.fish.translate("cli","cli","enemizer.not.found") + ': ' + args.enemizercli + "\n"
logging.warning("No Enemizer options will be applied until this is resolved.") 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: if args.race:
patch_race_rom(rom) patch_race_rom(rom)
@@ -219,13 +242,44 @@ def main(args, seed=None):
outfilepname += f'_P{player}' outfilepname += f'_P{player}'
if world.players > 1 or world.teams > 1: if world.players > 1 or world.teams > 1:
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else '' outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player], outfilestuffs = {
world.mode[player], world.goal[player], "logic": world.logic[player], # 0
"" if world.timer in ['none', 'display'] else "-" + world.timer, "difficulty": world.difficulty[player], # 1
world.shuffle[player], world.doorShuffle[player], world.algorithm, mcsb_name, "difficulty_adjustments": world.difficulty_adjustments[player], # 2
"-retro" if world.retro[player] else "", "mode": world.mode[player], # 3
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "goal": world.goal[player], # 4
"-nohints" if not world.hints[player] else "")) if not args.outputname else '' "timer": str(world.timer), # 5
"shuffle": world.shuffle[player], # 6
"doorShuffle": world.doorShuffle[player], # 7
"algorithm": world.algorithm, # 8
"mscb": mcsb_name, # 9
"retro": world.retro[player], # A
"progressive": world.progressive, # B
"hints": 'True' if world.hints[player] else 'False' # C
}
# 0 1 2 3 4 5 6 7 8 9 A B C
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s_%s-%s%s%s%s%s' % (
# 0 1 2 3 4 5 6 7 8 9 A B C
# _noglitches_normal-normal-open-ganon-ohko_simple_basic-balanced-keysanity-retro-prog_swords-nohints
# _noglitches_normal-normal-open-ganon _simple_basic-balanced-keysanity-retro
# _noglitches_normal-normal-open-ganon _simple_basic-balanced-keysanity -prog_swords
# _noglitches_normal-normal-open-ganon _simple_basic-balanced-keysanity -nohints
outfilestuffs["logic"], # 0
outfilestuffs["difficulty"], # 1
outfilestuffs["difficulty_adjustments"], # 2
outfilestuffs["mode"], # 3
outfilestuffs["goal"], # 4
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
outfilestuffs["shuffle"], # 6
outfilestuffs["doorShuffle"], # 7
outfilestuffs["algorithm"], # 8
outfilestuffs["mscb"], # 9
"-retro" if outfilestuffs["retro"] == "True" else "", # A
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # B
"-nohints" if not outfilestuffs["hints"] == "True" else "")) if not args.outputname else '' # C
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')) rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
if world.players > 1: if world.players > 1:
@@ -241,20 +295,34 @@ def main(args, seed=None):
with open(output_path('%s_multidata' % outfilebase), 'wb') as f: with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
f.write(multidata) 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: if not args.skip_playthrough:
logger.info('Calculating playthrough.') logger.info(world.fish.translate("cli","cli","calc.playthrough"))
create_playthrough(world) create_playthrough(world)
if args.jsonout: if args.jsonout:
print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()})) print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()}))
elif args.create_spoiler and not args.skip_playthrough: elif args.create_spoiler:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) logger.info(world.fish.translate("cli","cli","patching.spoiler"))
if args.jsonout:
with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile:
outfile.write(world.spoiler.to_json())
else:
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
logger.info('Done. Enjoy.') YES = world.fish.translate("cli","cli","yes")
logger.info('Total Time: %s', time.perf_counter() - start) 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","used.enemizer") % (YES if enemized 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 return world
@@ -383,7 +451,7 @@ def copy_dynamic_regions_and_locations(world, ret):
new_loc.always_allow = location.always_allow new_loc.always_allow = location.always_allow
new_loc.item_rule = location.item_rule new_loc.item_rule = location.item_rule
new_reg.locations.append(new_loc) new_reg.locations.append(new_loc)
ret.clear_location_cache() ret.clear_location_cache()
@@ -398,7 +466,7 @@ def create_playthrough(world):
collection_spheres = [] collection_spheres = []
state = CollectionState(world) state = CollectionState(world)
sphere_candidates = list(prog_locations) sphere_candidates = list(prog_locations)
logging.getLogger('').debug('Building up collection spheres.') logging.getLogger('').debug(world.fish.translate("cli","cli","building.collection.spheres"))
while sphere_candidates: while sphere_candidates:
state.sweep_for_events(key_only=True) state.sweep_for_events(key_only=True)
state.sweep_for_crystal_access() state.sweep_for_crystal_access()
@@ -417,11 +485,11 @@ def create_playthrough(world):
state_cache.append(state.copy()) state_cache.append(state.copy())
logging.getLogger('').debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(prog_locations)) logging.getLogger('').debug(world.fish.translate("cli","cli","building.calculating.spheres"), len(collection_spheres), len(sphere), len(prog_locations))
if not sphere: if not sphere:
logging.getLogger('').debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) logging.getLogger('').debug(world.fish.translate("cli","cli","cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
raise RuntimeError('Not all progression items reachable. Something went terribly wrong here.') raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.progression"))
else: else:
old_world.spoiler.unreachables = sphere_candidates.copy() old_world.spoiler.unreachables = sphere_candidates.copy()
break break
@@ -473,9 +541,9 @@ def create_playthrough(world):
collection_spheres.append(sphere) collection_spheres.append(sphere)
logging.getLogger('').debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations)) logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations))
if not sphere: if not sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
# store the required locations for statistical analysis # store the required locations for statistical analysis
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere] old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
+8 -3
View File
@@ -5,8 +5,9 @@ import urllib.request
import urllib.parse import urllib.parse
import re import re
from DungeonRandomizer import parse_arguments from DungeonRandomizer import parse_cli
from Main import main as DRMain from Main import main as DRMain
from source.classes.BabelFish import BabelFish
def parse_yaml(txt): def parse_yaml(txt):
def strip(s): def strip(s):
@@ -71,7 +72,7 @@ def main():
weights_cache[path] = get_weights(path) weights_cache[path] = get_weights(path)
print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}") 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.seed = seed
erargs.names = args.names erargs.names = args.names
erargs.create_spoiler = args.create_spoiler erargs.create_spoiler = args.create_spoiler
@@ -100,7 +101,7 @@ def main():
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.loglevel] loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[erargs.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel) logging.basicConfig(format='%(message)s', level=loglevel)
DRMain(erargs, seed) DRMain(erargs, seed, BabelFish())
def get_weights(path): def get_weights(path):
try: try:
@@ -150,6 +151,10 @@ def roll_settings(weights):
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.experimental = get_choice('experimental') == 'on' ret.experimental = get_choice('experimental') == 'on'
ret.dungeon_counters = get_choice('dungeon_counters')
if ret.dungeon_counters == 'default':
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
goal = get_choice('goals') goal = get_choice('goals')
ret.goal = {'ganon': 'ganon', ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals', 'fast_ganon': 'crystals',
+29
View File
@@ -0,0 +1,29 @@
# New Features
* Mirror Scroll no longer erases blocks, the real mirror still will. (Sorry!)
* Standard+Crossed Dungeon now gives you a little magic, a few bombs, and a few arrows if you die or S&Q after meeting your uncle (also works with mirror/scroll)
* Dungeon reminder added to hud for Crossed dungeons
* Blinking red square added to hud and it indicates a boss room is close by. Only appears if you have the compass. (Basic & Crossed)
* Agahnims dungeon items can be started with now
* GUI updates courtesy of Mike T
## Map Features (Crossed only + Experimental)
* Key counters added to hud. Indicates number of keys in chests (found/total). In small key shuffle, this count indicates how many smalls for that dungeon could be outside it.
* Total key indicator added to hud if you have found the map. Counts down from the total number of keys in dungeon to 0 as you collect them.
* Big Key indicator added to hud. Indicates if BK is not in the dungeon, or if BnC guard has it (Probably will move away from hud if kept)
Note: Only one of the key indicator will probably become core at most.
## Experimental changes
* Mirror scroll is now core for non-vanilla Door Shuffle (no longer experimental)
* GT Bosses stay dead in non-vanilla Door Shuffle (no longer experimental)
* Map features listed above are now experimental
# Bug Fixes
* Splashing at hobo no longer prevents you from buying bomb capacity upgrades
* Small vitreous eyeballs will not drop items (DR basic and crossed only)
* In Vanilla doors the HC back hallway area was broken - should be better now - also Trap Doors
* Firebar speed should now be consistent. Ice palace rooms have slow firebars even if shuffled to other dungeons. Others should have normal speed firebars.
+2 -1
View File
@@ -540,7 +540,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Mire 2', 'Misery Mire', None, ['Mire 2 Up Stairs', 'Mire 2 NE']), create_dungeon_region(player, 'Mire 2', 'Misery Mire', None, ['Mire 2 Up Stairs', 'Mire 2 NE']),
create_dungeon_region(player, 'Mire Hub', 'Misery Mire', None, ['Mire Hub SE', 'Mire Hub ES', 'Mire Hub E', 'Mire Hub NE', 'Mire Hub WN', 'Mire Hub WS', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier']), create_dungeon_region(player, 'Mire Hub', 'Misery Mire', None, ['Mire Hub SE', 'Mire Hub ES', 'Mire Hub E', 'Mire Hub NE', 'Mire Hub WN', 'Mire Hub WS', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier']),
create_dungeon_region(player, 'Mire Hub Right', 'Misery Mire', None, ['Mire Hub Right EN', 'Mire Hub Right Blue Barrier']), create_dungeon_region(player, 'Mire Hub Right', 'Misery Mire', None, ['Mire Hub Right EN', 'Mire Hub Right Blue Barrier']),
create_dungeon_region(player, 'Mire Hub Top', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Top NW', 'Mire Hub Top Blue Barrier']), create_dungeon_region(player, 'Mire Hub Top', 'Misery Mire', None, ['Mire Hub Top NW', 'Mire Hub Top Blue Barrier']),
create_dungeon_region(player, 'Mire Hub Switch', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Switch Blue Barrier N', 'Mire Hub Switch Blue Barrier S']),
create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']), create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']),
create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']), create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']),
create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']), create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']),
+42 -10
View File
@@ -10,7 +10,7 @@ import sys
import subprocess import subprocess
from BaseClasses import CollectionState, ShopType, Region, Location, DoorType from BaseClasses import CollectionState, ShopType, Region, Location, DoorType
from DoorShuffle import compass_data, DROptions from DoorShuffle import compass_data, DROptions, boss_indicator
from Dungeons import dungeon_music_addresses from Dungeons import dungeon_music_addresses
from Regions import location_table from Regions import location_table
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
@@ -22,7 +22,7 @@ from EntranceShuffle import door_addresses, exit_ids
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '5e01caffabb4509a0987ef2f2f0bcd56' RANDOMIZERBASEHASH = 'a4d716a9c9b3299267deee4ddb1143a5'
class JsonRom(object): class JsonRom(object):
@@ -78,6 +78,8 @@ class LocalRom(object):
self.name = name self.name = name
self.hash = hash self.hash = hash
self.orig_buffer = None 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: with open(file, 'rb') as stream:
self.buffer = read_rom(stream) self.buffer = read_rom(stream)
if patch: if patch:
@@ -160,7 +162,7 @@ def read_rom(stream):
def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit): def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit):
baserom_path = os.path.abspath(baserom_path) baserom_path = os.path.abspath(baserom_path)
basepatch_path = os.path.abspath(local_path('data/base2current.json')) basepatch_path = os.path.abspath(local_path(os.path.join("data","base2current.json")))
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json") enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json')) randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
options_path = os.path.abspath(output_path('enemizer_options.json')) options_path = os.path.abspath(output_path('enemizer_options.json'))
@@ -303,7 +305,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
_sprite_table = {} _sprite_table = {}
def _populate_sprite_table(): def _populate_sprite_table():
if not _sprite_table: if not _sprite_table:
for dir in [local_path('data/sprites/official'), local_path('data/sprites/unofficial')]: for dir in [local_path(os.path.join("data","sprites","official")), local_path(os.path.join("data","sprites","unofficial"))]:
for file in os.listdir(dir): for file in os.listdir(dir):
filepath = os.path.join(dir, file) filepath = os.path.join(dir, file)
if not os.path.isfile(filepath): if not os.path.isfile(filepath):
@@ -383,7 +385,7 @@ class Sprite(object):
@staticmethod @staticmethod
def default_link_sprite(): def default_link_sprite():
return Sprite(local_path('data/default.zspr')) return get_sprite_from_name('Link')
def decode8(self, pos): def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)] arr = [[0 for _ in range(8)] for _ in range(8)]
@@ -590,10 +592,22 @@ def patch_rom(world, rom, player, team, enemized):
if world.mode[player] == 'inverted': if world.mode[player] == 'inverted':
patch_shuffled_dark_sanc(world, rom, player) patch_shuffled_dark_sanc(world, rom, player)
# setup dr option flags based on experimental, etc.
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
if world.experimental[player]:
dr_flags |= DROptions.Map_Info
# patch doors # patch doors
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' or not world.experimental[player] else DROptions.Town_Portal
if world.doorShuffle[player] == 'crossed': if world.doorShuffle[player] == 'crossed':
rom.write_byte(0x139004, 2) rom.write_byte(0x139004, 2)
for name, layout in world.key_layout[player].items():
offset = compass_data[name][4]//2
rom.write_byte(0x13f01c+offset, layout.max_chests + layout.max_drops)
rom.write_byte(0x13f02a+offset, layout.max_chests)
builder = world.dungeon_layouts[player][name]
bk_status = 1 if builder.bk_required else 0
bk_status = 2 if builder.bk_provided else bk_status
rom.write_byte(0x13f038+offset*2, bk_status)
rom.write_byte(0x151f1, 2) rom.write_byte(0x151f1, 2)
rom.write_byte(0x15270, 2) rom.write_byte(0x15270, 2)
rom.write_byte(0x1597b, 2) rom.write_byte(0x1597b, 2)
@@ -617,6 +631,13 @@ def patch_rom(world, rom, player, team, enemized):
if builder.pre_open_stonewall: if builder.pre_open_stonewall:
if builder.pre_open_stonewall.name == 'Desert Wall Slide NW': if builder.pre_open_stonewall.name == 'Desert Wall Slide NW':
dr_flags |= DROptions.Open_Desert_Wall dr_flags |= DROptions.Open_Desert_Wall
for name, pair in boss_indicator.items():
dungeon_id, boss_door = pair
opposite_door = world.get_door(boss_door, player).dest
if opposite_door.roomIndex > -1:
dungeon_name = opposite_door.entrance.parent_region.dungeon.name
dungeon_id = boss_indicator[dungeon_name][0]
rom.write_byte(0x13f000+dungeon_id, opposite_door.roomIndex)
rom.write_byte(0x139006, dr_flags.value) rom.write_byte(0x139006, dr_flags.value)
if dr_flags & DROptions.Town_Portal and world.mode[player] == 'inverted': if dr_flags & DROptions.Town_Portal and world.mode[player] == 'inverted':
rom.write_byte(0x139008, 1) rom.write_byte(0x139008, 1)
@@ -759,10 +780,10 @@ def patch_rom(world, rom, player, team, enemized):
difficulty.progressive_shield_limit, overflow_replacement, difficulty.progressive_shield_limit, overflow_replacement,
difficulty.progressive_armor_limit, overflow_replacement, difficulty.progressive_armor_limit, overflow_replacement,
difficulty.progressive_bottle_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 #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]) rom.write_bytes(0x180098, [difficulty.progressive_bow_limit, overflow_replacement])
if difficulty.progressive_bow_limit < 2 and world.swords == 'swordless': if difficulty.progressive_bow_limit < 2 and world.swords == 'swordless':
rom.write_bytes(0x180098, [2, overflow_replacement]) rom.write_bytes(0x180098, [2, overflow_replacement])
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
@@ -1043,6 +1064,7 @@ def patch_rom(world, rom, player, team, enemized):
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10), 'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10),
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20), 'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20),
'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0), 'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0),
'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08), 'Map (Agahnims Tower)': (0x369, 0x08),
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02), 'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02),
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10), 'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10),
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80), 'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80),
@@ -1227,6 +1249,7 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_bytes(0x180185, [0,0,0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0,0,0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0,0,0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0,0,0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0,0,0]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0,0,0]) # Mantle respawn refills (magic, bombs, arrows)
bow_max, bomb_max, magic_max = 0, 0, 0
if world.mode[player] == 'standard': if world.mode[player] == 'standard':
if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']:
rom.write_byte(0x18004E, 1) # Escape Fill (arrows) rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
@@ -1234,16 +1257,25 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_bytes(0x180185, [0,0,70]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0,0,70]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0,0,10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0,0,10]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0,0,10]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0,0,10]) # Mantle respawn refills (magic, bombs, arrows)
bow_max = 70
elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']: elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']:
rom.write_byte(0x18004E, 2) # Escape Fill (bombs) rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
rom.write_bytes(0x180185, [0,50,0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0,50,0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0,3,0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0,3,0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0,3,0]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0,3,0]) # Mantle respawn refills (magic, bombs, arrows)
bomb_max = 50
elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']:
rom.write_byte(0x18004E, 4) # Escape Fill (magic) rom.write_byte(0x18004E, 4) # Escape Fill (magic)
rom.write_bytes(0x180185, [0x80,0,0]) # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [0x80,0,0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0x20,0,0]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x180188, [0x20,0,0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0x20,0,0]) # Mantle respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0x20,0,0]) # Mantle respawn refills (magic, bombs, arrows)
magic_max = 0x80
if world.doorShuffle[player] == 'crossed':
# Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0x20, 3, 10]) # Mantle respawn refills (magic, bombs, arrows)
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved # patch swamp: Need to enable permanent drain of water as dam or swamp were moved
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00) rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00)
@@ -2089,9 +2121,9 @@ def patch_shuffled_dark_sanc(world, rom, player):
dark_sanc_entrance = str(world.get_region('Inverted Dark Sanctuary', player).entrances[0].name) 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] 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] door_index = door_addresses[str(dark_sanc_entrance)][0]
rom.write_byte(0x180241, 0x01) 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) write_int16(rom, 0x180250, room_id)
rom.write_byte(0x180252, ow_area) rom.write_byte(0x180252, ow_area)
write_int16s(rom, 0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x]) write_int16s(rom, 0x180253, [vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x])
+8 -2
View File
@@ -342,6 +342,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Mire Hub Lower Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player)) set_rule(world.get_entrance('Mire Hub Lower Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub', player), player))
set_rule(world.get_entrance('Mire Hub Right Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Right', player), player)) set_rule(world.get_entrance('Mire Hub Right Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Right', player), player))
set_rule(world.get_entrance('Mire Hub Top Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Top', player), player)) set_rule(world.get_entrance('Mire Hub Top Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Top', player), player))
set_rule(world.get_entrance('Mire Hub Switch Blue Barrier N', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Switch', player), player))
set_rule(world.get_entrance('Mire Hub Switch Blue Barrier S', player), lambda state: state.can_reach_blue(world.get_region('Mire Hub Switch', player), player))
set_rule(world.get_entrance('Mire Map Spike Side Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spike Side', player), player)) set_rule(world.get_entrance('Mire Map Spike Side Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spike Side', player), player))
set_rule(world.get_entrance('Mire Map Spot Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spot', player), player)) set_rule(world.get_entrance('Mire Map Spot Blue Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Map Spot', player), player))
set_rule(world.get_entrance('Mire Crystal Dead End Left Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player)) set_rule(world.get_entrance('Mire Crystal Dead End Left Barrier', player), lambda state: state.can_reach_blue(world.get_region('Mire Crystal Dead End', player), player))
@@ -1516,7 +1518,8 @@ bunny_impassible_doors = {
'Ice Crystal Right Blue Hole', 'Ice Crystal Left Blue Barrier', 'Ice Big Chest Landing Push Blocks', 'Ice Crystal Right Blue Hole', 'Ice Crystal Left Blue Barrier', 'Ice Big Chest Landing Push Blocks',
'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap', 'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap',
'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier', 'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier',
'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Falling Bridge WN', 'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Hub Switch Blue Barrier N',
'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge WN',
'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', 'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier',
'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Hidden Shooters Block Path S', 'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Hidden Shooters Block Path S',
'Mire Hidden Shooters Block Path N', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier', 'Mire Hidden Shooters Block Path N', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
@@ -1546,7 +1549,10 @@ def add_key_logic_rules(world, player):
key_logic = world.key_logic[player] key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items(): for d_name, d_logic in key_logic.items():
for door_name, keys in d_logic.door_rules.items(): for door_name, keys in d_logic.door_rules.items():
add_rule(world.get_entrance(door_name, player), create_advanced_key_rule(d_logic, player, keys)) spot = world.get_entrance(door_name, player)
add_rule(spot, create_advanced_key_rule(d_logic, player, keys))
if keys.opposite:
add_rule(spot, create_advanced_key_rule(d_logic, player, keys.opposite), 'or')
for location in d_logic.bk_restricted: for location in d_logic.bk_restricted:
if location.name not in key_only_locations.keys(): if location.name not in key_only_locations.keys():
forbid_item(location, d_logic.bk_name, player) forbid_item(location, d_logic.bk_name, player)
+131 -16
View File
@@ -76,12 +76,16 @@ def output_path(path):
NSUserDomainMask = 1 NSUserDomainMask = 1
# True for expanding the tilde into a fully qualified path # True for expanding the tilde into a fully qualified path
documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0] 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: else:
raise NotImplementedError('Not supported yet') 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): 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) return os.path.join(output_path.cached_path, path)
output_path.cached_path = None output_path.cached_path = None
@@ -201,8 +205,7 @@ def read_entrance_data(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan)
print(string) 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(): for d, region_list in d_regions.items():
tile_map = {} tile_map = {}
for region in region_list: for region in region_list:
@@ -217,28 +220,140 @@ def print_wiki_doors(d_regions, world, player):
if tile not in tile_map: if tile not in tile_map:
tile_map[tile] = [] tile_map[tile] = []
tile_map[tile].append(r) tile_map[tile].append(r)
print(d) toprint = ""
print('{| class="wikitable"') toprint += ('<!-- ' + d + ' -->') + "\n"
print('|-') toprint += ('== Room List ==') + "\n"
print('! Room') toprint += "\n"
print('! Supertile') toprint += ('{| class="wikitable"') + "\n"
print('! Doors') toprint += ('|-') + "\n"
toprint += ('! Room !! Supertile !! Doors') + "\n"
for tile, region_list in tile_map.items(): for tile, region_list in tile_map.items():
tile_done = False tile_done = False
for region in region_list: for region in region_list:
print('|-') toprint += ('|-') + "\n"
print('| '+region.name) toprint += ('| {{Dungeon Room|{{PAGENAME}}|' + region.name + '}}') + "\n"
if not tile_done: if not tile_done:
listlen = len(region_list) listlen = len(region_list)
link = '| {{UnderworldMapLink|'+str(tile)+'}}' 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 tile_done = True
strs_to_print = [] strs_to_print = []
for ext in region.exits: for ext in region.exits:
strs_to_print.append(ext.name) strs_to_print.append('{{Dungeon Door|{{PAGENAME}}|' + ext.name + '}}')
print('| '+' <br /> '.join(strs_to_print)) toprint += ('| '+'<br />'.join(strs_to_print))
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):
if args:
argVars = vars(args)
truthy = [ 1, True, "True", "true" ]
# Hints default to TRUE
# Don't do: Yes
# Do: No
if "no_hints" in argVars:
src = "no_hints"
if isinstance(argVars["hints"],dict):
tmp = {}
for idx in range(1,len(argVars["hints"]) + 1):
tmp[idx] = argVars[src] not in truthy # tmp = !src
args.hints = tmp # dest = tmp
else:
args.hints = args.no_hints not in truthy # dest = !src
# Don't do: No
# Do: Yes
if "hints" in argVars:
src = "hints"
if isinstance(argVars["hints"],dict):
tmp = {}
for idx in range(1,len(argVars["hints"]) + 1):
tmp[idx] = argVars[src] not in truthy # tmp = !src
args.no_hints = tmp # dest = tmp
else:
args.no_hints = args.hints not in truthy # dest = !src
# Spoiler defaults to FALSE
# Don't do: No
# Do: Yes
if "create_spoiler" in argVars:
args.suppress_spoiler = not args.create_spoiler in truthy
# Don't do: Yes
# Do: No
if "suppress_spoiler" in argVars:
args.create_spoiler = not args.suppress_spoiler in truthy
# ROM defaults to TRUE
# Don't do: Yes
# Do: No
if "suppress_rom" in argVars:
args.create_rom = not args.suppress_rom in truthy
# Don't do: No
# Do: Yes
if "create_rom" in argVars:
args.suppress_rom = not args.create_rom in truthy
# Shuffle Ganon defaults to TRUE
# 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
# Playthrough defaults to TRUE
# 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): def print_xml_doors(d_regions, world, player):
root = ET.Element('root') root = ET.Element('root')
+1
View File
@@ -21,6 +21,7 @@ incsrc keydoors.asm
incsrc overrides.asm incsrc overrides.asm
;incsrc edges.asm ;incsrc edges.asm
;incsrc math.asm ;incsrc math.asm
incsrc hudadditions.asm
warnpc $279000 warnpc $279000
; Data Section ; Data Section
+18 -1
View File
@@ -540,4 +540,21 @@ db $a0,$a0,$50 ; DP Main Lobby
db $58,$50,$30, $98,$50,$70 ; TT Ambush db $58,$50,$30, $98,$50,$70 ; TT Ambush
db $58,$50,$30 ; TT Nook db $58,$50,$30 ; TT Nook
MultDivInfo: ; (1placeholder, 1, 2, 3, 4, 5, 6, 10, 20) MultDivInfo: ; (1placeholder, 1, 2, 3, 4, 5, 6, 10, 20)
db $01, $01, $02, $03, $04, $05, $06, $0a, $14 db $01, $01, $02, $03, $04, $05, $06, $0a, $14
; dungeon tables
; HC HC EP DP AT SP PD MM SW IP TH TT TR GT
org $27f000
CompassBossIndicator:
dw $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000, $0000
TotalKeys: ;27f01c
db $04, $04, $02, $04, $04, $06, $06, $06, $05, $06, $01, $03, $06, $08
ChestKeys: ;27f02a
db $01, $01, $00, $01, $02, $01, $06, $03, $03, $02, $01, $01, $04, $04
BigKeyStatus: ;27f038 (status 2 indicate BnC guard)
dw $0002, $0002, $0001, $0001, $0000, $0001, $0001, $0001, $0001, $0001, $0001, $0001, $0001, $0001
DungeonReminderTable: ;27f054
dw $2D50, $2D50, $2D51, $2D52, $2D54, $2D56, $2D55, $2D5A, $2D57, $2D59, $2D53, $2D58, $2D5B, $2D5C
;27f070
+30 -4
View File
@@ -11,12 +11,14 @@ jsl AdjustTransition
nop nop
;turn off linking doors -- see .notRoomLinkDoor label in Bank02.asm ;turn off linking doors -- see .notRoomLinkDoor label in Bank02.asm
org $02b5a6 org $02b5a8 ; <- 135a8 - Bank02.asm : 8368 (LDA $7EC004 : STA $A0)
bra NotLinkDoor1 jsl CheckLinkDoorR
bcc NotLinkDoor1
org $02b5b6 org $02b5b6
NotLinkDoor1: NotLinkDoor1:
org $02b647 org $02b649 ; <- 135a8 - Bank02.asm : 8482 (LDA $7EC004 : STA $A0)
bra NotLinkDoor2 jsl CheckLinkDoorL
bcc NotLinkDoor2
org $02b657 org $02b657
NotLinkDoor2: NotLinkDoor2:
@@ -77,7 +79,31 @@ org $2081f2
jsl MirrorCheckOverride2 jsl MirrorCheckOverride2
org $20825c org $20825c
jsl MirrorCheckOverride2 jsl MirrorCheckOverride2
org $07a955 ; <- Bank07.asm : around 6564 (JP is a bit different) (STZ $05FC : STZ $05FD)
jsl BlockEraseFix
nop #2
org $02b82a
jsl FixShopCode
org $1ddeea ; <- Bank1D.asm : 286 (JSL Sprite_LoadProperties)
jsl VitreousKeyReset
org $1ed024 ; f5024 sprite_guruguru_bar.asm : 27 (LDA $040C : CMP.b #$12 : INY #2
jsl GuruguruFix : bra .next
nop #3
.next
; also rando's hooks.asm line 1360
org $a0ee11 ; <- 6FC4C - headsup_display.asm : 836 (LDA $7EF36E : AND.w #$00FF : ADD.w #$0007 : AND.w #$FFF8 : TAX)
jsl DrHudOverride
org $098638 ; rando's hooks.asm line 2192
jsl CountChestKeys
org $06D192 ; rando's hooks.asm line 457
jsl CountAbsorbedKeys
; rando's hooks.asm line 1020
org $05FC7E ; <- 2FC7E - sprite_dash_item.asm : 118 (LDA $7EF36F : INC A : STA $7EF36F)
jsl CountBonkItem
; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire ; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire
; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes ; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes
+96
View File
@@ -0,0 +1,96 @@
DrHudOverride:
{
jsl.l NewDrawHud
jsr HudAdditions
rtl
}
HudAdditions:
{
ldx $040c : cpx #$ff : bne + : rts : +
lda DRMode : bne + : rts : +
phb : phk : plb
lda $7ef364 : and.l $0098c0, x : beq +
lda CompassBossIndicator, x : and #$00ff : cmp $a0 : bne +
lda $1a : and #$0010 : beq +
lda #$345e : sta $7ec790 : bra .next
+ lda #$207f : sta $7ec790
.next lda DRMode : and #$0002 : bne + : plb : rts : +
lda $7ef36d : and #$00ff : beq +
lda DungeonReminderTable, x : bra .reminder
+ lda #$207f
.reminder sta $7ec702
+ lda DRFlags : and #$0004 : beq .restore
lda $7ef368 : and.l $0098c0, x : beq .restore
lda #$2811 : sta $7ec740
lda $7ef366 : and.l $0098c0, x : bne .check
lda BigKeyStatus, x : and #$00ff : bne + ;todo: "and" is redundant or change table to one byte
lda #$2574 : bra ++
+ cmp #$0002 : bne +
lda #$2420 : bra ++
+ lda #$207f : bra ++
.check lda #$2826
++ sta $7ec742
txa : lsr : tax
lda $7ef4e0, x : jsr ConvertToDisplay : sta $7ec7a2
lda #$2830 : sta $7ec7a4
lda ChestKeys, x : jsr ConvertToDisplay : sta $7ec7a6
lda #$2871 : sta $7ec780
lda TotalKeys, x
sep #$20 : !sub $7ef4b0, x : rep #$20
jsr ConvertToDisplay : sta $7ec782
.restore
plb : rts
}
ConvertToDisplay:
and #$00ff : cmp #$000a : !blt +
!add #$2553 : rts
+ !add #$2490 : rts
CountChestKeys:
jsl ItemDowngradeFix
jsr CountChest
rtl
CountChest:
cpy #$24 : beq +
cpy #$a0 : !blt .end
cpy #$ae : !bge .end
pha : phx
tya : and #$0f : bne ++
inc a
++ tax : bra .count
+ pha : phx
lda $040c : lsr : tax
.count
lda $7ef4b0, x : inc : sta $7ef4b0, x
lda $7ef4e0, x : inc : sta $7ef4e0, x
.restore plx : pla
.end rts
CountAbsorbedKeys:
jsl IncrementSmallKeysNoPrimary : phx
lda $040c : cmp #$ff : beq +
lsr : tax
lda $7ef4b0, x : inc : sta $7ef4b0, x
+ plx : rtl
CountBonkItem:
jsl GiveBonkItem
lda $a0 ; check room ID - only bonk keys in 2 rooms so we're just checking the lower byte
cmp #115 : bne + ; Desert Bonk Key
lda.l BonkKey_Desert
bra ++
+ : cmp #140 : bne + ; GTower Bonk Key
lda.l BonkKey_GTower
bra ++
+ lda.b #$24 ; default to small key
++
phy : tay : jsr CountChest : ply
rtl
+3 -2
View File
@@ -12,8 +12,9 @@
CheckIfDoorsOpen: { CheckIfDoorsOpen: {
jsr TrapDoorFixer ; see normal.asm jsr TrapDoorFixer ; see normal.asm
; note we are 16bit mode right now ; note we are 16bit mode right now
lda $040c : cmp #$00ff : bne .gtg lda DRMode : beq +
lda $a0 : dec : tax : and #$000f ; hijacked code lda $040c : cmp #$00ff : bne .gtg
+ lda $a0 : dec : tax : and #$000f ; hijacked code
sec : rtl ; set carry to indicate normal behavior sec : rtl ; set carry to indicate normal behavior
.gtg .gtg
+14
View File
@@ -42,6 +42,20 @@ WarpDown:
jsr Cleanup jsr Cleanup
rtl rtl
; carry set = use link door like normal
; carry clear = we are in dr mode, never use linking doors
CheckLinkDoorR:
lda DRMode : bne +
lda $7ec004 : sta $a0 ; what we wrote over
sec : rtl
+ clc : rtl
CheckLinkDoorL:
lda DRMode : bne +
lda $7ec003 : sta $a0 ; what we wrote over
sec : rtl
+ clc : rtl
TrapDoorFixer: TrapDoorFixer:
lda $fe : and #$0038 : beq .end lda $fe : and #$0038 : beq .end
xba : asl #2 : sta $00 xba : asl #2 : sta $00
+24 -1
View File
@@ -50,4 +50,27 @@ MirrorCheckOverride:
+ lda DRScroll : rtl + lda DRScroll : rtl
MirrorCheckOverride2: MirrorCheckOverride2:
lda $7ef353 : and #$02 : rtl lda $7ef353 : and #$02 : rtl
BlockEraseFix:
lda $7ef353 : and #$02 : beq +
stz $05fc : stz $05fd
+ rtl
FixShopCode:
cpx #$300 : !bge +
sta $7ef000, x
+ rtl
VitreousKeyReset:
lda DRMode : beq +
stz $0cba, x
+ jsl $0db818 ;restore old code
rtl
GuruguruFix:
lda $a0 : cmp #$df : !bge +
and #$0f : cmp #$0e : !blt +
iny #2
+ rtl
+5
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)
+9 -2
View File
@@ -1,18 +1,25 @@
import subprocess import subprocess
import os import os
import shutil import shutil
import sys
# Spec file
SPEC_FILE = os.path.join("DungeonRandomizer.spec")
# Destination is current dir
DEST_DIRECTORY = '.' DEST_DIRECTORY = '.'
# Check for UPX
if os.path.isdir("upx"): if os.path.isdir("upx"):
upx_string = "--upx-dir=upx" upx_string = "--upx-dir=upx"
else: else:
upx_string = "" 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") shutil.rmtree("build")
subprocess.run(" ".join(["pyinstaller DungeonRandomizer.spec ", # Run pyinstaller for DungeonRandomizer
subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ",
upx_string, upx_string,
"-y ", "-y ",
"--onefile ", "--onefile ",
+9 -2
View File
@@ -1,18 +1,25 @@
import subprocess import subprocess
import os import os
import shutil import shutil
import sys
# Spec file
SPEC_FILE = os.path.join("Gui.spec")
# Destination is current dir
DEST_DIRECTORY = '.' DEST_DIRECTORY = '.'
# Check for UPX
if os.path.isdir("upx"): if os.path.isdir("upx"):
upx_string = "--upx-dir=upx" upx_string = "--upx-dir=upx"
else: else:
upx_string = "" 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") shutil.rmtree("build")
subprocess.run(" ".join(["pyinstaller Gui.spec ", # Run pyinstaller for Gui
subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ",
upx_string, upx_string,
"-y ", "-y ",
"--onefile ", "--onefile ",
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "classes" package
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "gui" package
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "gui.about" package
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "gui.adjust" package
-147
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
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "gui.custom" package
-74
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])
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "gui.randomize" package
-63
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
-45
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
-38
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
-1
View File
@@ -1 +0,0 @@
# do nothing, just exist to make "gui.startinventory" package
-3
View File
@@ -1,3 +0,0 @@
aioconsole==0.1.15
colorama==0.4.3
websockets==8.1
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "resources" package
+337
View File
@@ -0,0 +1,337 @@
{
"lang": {},
"diags": {
"action": "store_true",
"type": "bool"
},
"create_spoiler": {
"action": "store_true",
"type": "bool"
},
"suppress_spoiler": {
"action": "store_false",
"dest": "create_spoiler",
"help": "suppress"
},
"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_false",
"type": "bool"
},
"no_hints": {
"action": "store_true",
"dest": "hints",
"help": "suppress"
},
"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": {}
}
+27
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."
}
}
+279
View File
@@ -0,0 +1,279 @@
{
"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",
"used.enemizer": "Enemized: %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)"
]
}
}
+34
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."
}
}
+4 -69
View File
@@ -1,32 +1,11 @@
{ {
"checkboxes": { "checkboxes": {
"nobgm": { "nobgm": { "type": "checkbox" },
"type": "checkbox", "quickswap": { "type": "checkbox" }
"label": {
"text": "Disable Music & MSU-1"
}
},
"quickswap": {
"type": "checkbox",
"label": {
"text": "L/R Quickswapping"
}
}
}, },
"leftAdjustFrame": { "leftAdjustFrame": {
"heartcolor": { "heartcolor": {
"type": "selectbox", "type": "selectbox",
"label": {
"text": "Heart Color"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": { "options": {
"Red": "red", "Red": "red",
"Blue": "blue", "Blue": "blue",
@@ -37,18 +16,7 @@
}, },
"heartbeep": { "heartbeep": {
"type": "selectbox", "type": "selectbox",
"label": { "default": "Normal",
"text": "Heart Beep sound rate"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": { "options": {
"Double": "double", "Double": "double",
"Normal": "normal", "Normal": "normal",
@@ -61,18 +29,7 @@
"rightAdjustFrame": { "rightAdjustFrame": {
"menuspeed": { "menuspeed": {
"type": "selectbox", "type": "selectbox",
"label": { "default": "Normal",
"text": "Menu Speed"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": { "options": {
"Instant": "instant", "Instant": "instant",
"Quadruple": "quadruple", "Quadruple": "quadruple",
@@ -84,17 +41,6 @@
}, },
"owpalettes": { "owpalettes": {
"type": "selectbox", "type": "selectbox",
"label": {
"text": "Overworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": { "options": {
"Default": "default", "Default": "default",
"Random": "random", "Random": "random",
@@ -103,17 +49,6 @@
}, },
"uwpalettes": { "uwpalettes": {
"type": "selectbox", "type": "selectbox",
"label": {
"text": "Underworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": { "options": {
"Default": "default", "Default": "default",
"Random": "random", "Random": "random",
+72 -427
View File
@@ -6,12 +6,7 @@
"text": "Bow" "text": "Bow"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"progressivebow": { "progressivebow": {
"type": "textbox", "type": "textbox",
@@ -19,12 +14,7 @@
"text": "Progressive Bow" "text": "Progressive Bow"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 2
"label": {
"sticky": "w"
},
"default": 2
}
}, },
"boomerang": { "boomerang": {
"type": "textbox", "type": "textbox",
@@ -32,12 +22,7 @@
"text": "Blue Boomerang" "text": "Blue Boomerang"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"redmerang": { "redmerang": {
"type": "textbox", "type": "textbox",
@@ -45,12 +30,7 @@
"text": "Red Boomerang" "text": "Red Boomerang"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"hookshot": { "hookshot": {
"type": "textbox", "type": "textbox",
@@ -58,12 +38,7 @@
"text": "Hookshot" "text": "Hookshot"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"mushroom": { "mushroom": {
"type": "textbox", "type": "textbox",
@@ -71,12 +46,7 @@
"text": "Mushroom" "text": "Mushroom"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"powder": { "powder": {
"type": "textbox", "type": "textbox",
@@ -84,12 +54,7 @@
"text": "Magic Powder" "text": "Magic Powder"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"firerod": { "firerod": {
"type": "textbox", "type": "textbox",
@@ -97,12 +62,7 @@
"text": "Fire Rod" "text": "Fire Rod"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"icerod": { "icerod": {
"type": "textbox", "type": "textbox",
@@ -110,12 +70,7 @@
"text": "Ice Rod" "text": "Ice Rod"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"bombos": { "bombos": {
"type": "textbox", "type": "textbox",
@@ -123,12 +78,7 @@
"text": "Bombos" "text": "Bombos"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"ether": { "ether": {
"type": "textbox", "type": "textbox",
@@ -136,12 +86,7 @@
"text": "Ether" "text": "Ether"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"quake": { "quake": {
"type": "textbox", "type": "textbox",
@@ -149,12 +94,7 @@
"text": "Quake" "text": "Quake"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"lamp": { "lamp": {
"type": "textbox", "type": "textbox",
@@ -162,12 +102,7 @@
"text": "Lamp" "text": "Lamp"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"hammer": { "hammer": {
"type": "textbox", "type": "textbox",
@@ -175,12 +110,7 @@
"text": "Hammer" "text": "Hammer"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"shovel": { "shovel": {
"type": "textbox", "type": "textbox",
@@ -188,12 +118,7 @@
"text": "Shovel" "text": "Shovel"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
} }
}, },
"itemList2": { "itemList2": {
@@ -203,12 +128,7 @@
"text": "Flute" "text": "Flute"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"bugnet": { "bugnet": {
"type": "textbox", "type": "textbox",
@@ -216,12 +136,7 @@
"text": "Bug Net" "text": "Bug Net"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"book": { "book": {
"type": "textbox", "type": "textbox",
@@ -229,12 +144,7 @@
"text": "Book" "text": "Book"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"bottle": { "bottle": {
"type": "textbox", "type": "textbox",
@@ -242,12 +152,7 @@
"text": "Bottle" "text": "Bottle"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 4
"label": {
"sticky": "w"
},
"default": 4
}
}, },
"somaria": { "somaria": {
"type": "textbox", "type": "textbox",
@@ -255,12 +160,7 @@
"text": "Cane of Somaria" "text": "Cane of Somaria"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"byrna": { "byrna": {
"type": "textbox", "type": "textbox",
@@ -268,12 +168,7 @@
"text": "Cane of Byrna" "text": "Cane of Byrna"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"cape": { "cape": {
"type": "textbox", "type": "textbox",
@@ -281,12 +176,7 @@
"text": "Magic Cape" "text": "Magic Cape"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"mirror": { "mirror": {
"type": "textbox", "type": "textbox",
@@ -294,12 +184,7 @@
"text": "Magic Mirror" "text": "Magic Mirror"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"boots": { "boots": {
"type": "textbox", "type": "textbox",
@@ -307,12 +192,7 @@
"text": "Pegasus Boots" "text": "Pegasus Boots"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"powerglove": { "powerglove": {
"type": "textbox", "type": "textbox",
@@ -320,12 +200,7 @@
"text": "Power Glove" "text": "Power Glove"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"titansmitt": { "titansmitt": {
"type": "textbox", "type": "textbox",
@@ -333,12 +208,7 @@
"text": "Titan's Mitt" "text": "Titan's Mitt"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"progressiveglove": { "progressiveglove": {
"type": "textbox", "type": "textbox",
@@ -346,12 +216,7 @@
"text": "Progressive Glove" "text": "Progressive Glove"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 2
"label": {
"sticky": "w"
},
"default": 2
}
}, },
"flippers": { "flippers": {
"type": "textbox", "type": "textbox",
@@ -359,12 +224,7 @@
"text": "Flippers" "text": "Flippers"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"pearl": { "pearl": {
"type": "textbox", "type": "textbox",
@@ -372,12 +232,7 @@
"text": "Moon Pearl" "text": "Moon Pearl"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"heartpiece": { "heartpiece": {
"type": "textbox", "type": "textbox",
@@ -385,12 +240,7 @@
"text": "Piece of Heart" "text": "Piece of Heart"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 24
"label": {
"sticky": "w"
},
"default": 24
}
} }
}, },
"itemList3": { "itemList3": {
@@ -400,12 +250,7 @@
"text": "Heart Container" "text": "Heart Container"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 10
"label": {
"sticky": "w"
},
"default": 10
}
}, },
"sancheart": { "sancheart": {
"type": "textbox", "type": "textbox",
@@ -413,12 +258,7 @@
"text": "Sanctuary Heart" "text": "Sanctuary Heart"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"sword1": { "sword1": {
"type": "textbox", "type": "textbox",
@@ -426,12 +266,7 @@
"text": "Fighters' Sword" "text": "Fighters' Sword"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"sword2": { "sword2": {
"type": "textbox", "type": "textbox",
@@ -439,12 +274,7 @@
"text": "Master Sword" "text": "Master Sword"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"sword3": { "sword3": {
"type": "textbox", "type": "textbox",
@@ -452,12 +282,7 @@
"text": "Tempered Sword" "text": "Tempered Sword"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"sword4": { "sword4": {
"type": "textbox", "type": "textbox",
@@ -465,12 +290,7 @@
"text": "Golden Sword" "text": "Golden Sword"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"progressivesword": { "progressivesword": {
"type": "textbox", "type": "textbox",
@@ -478,12 +298,7 @@
"text": "Progressive Sword" "text": "Progressive Sword"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 4
"label": {
"sticky": "w"
},
"default": 4
}
}, },
"shield1": { "shield1": {
"type": "textbox", "type": "textbox",
@@ -491,12 +306,7 @@
"text": "Fighters' Shield" "text": "Fighters' Shield"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"shield2": { "shield2": {
"type": "textbox", "type": "textbox",
@@ -504,12 +314,7 @@
"text": "Fire Shield" "text": "Fire Shield"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"shield3": { "shield3": {
"type": "textbox", "type": "textbox",
@@ -517,12 +322,7 @@
"text": "Mirror Shield" "text": "Mirror Shield"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"progressiveshield": { "progressiveshield": {
"type": "textbox", "type": "textbox",
@@ -530,12 +330,7 @@
"text": "Progressive Shield" "text": "Progressive Shield"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 3
"label": {
"sticky": "w"
},
"default": 3
}
}, },
"mail2": { "mail2": {
"type": "textbox", "type": "textbox",
@@ -543,12 +338,7 @@
"text": "Blue Mail" "text": "Blue Mail"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"mail3": { "mail3": {
"type": "textbox", "type": "textbox",
@@ -556,12 +346,7 @@
"text": "Red Mail" "text": "Red Mail"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"progressivemail": { "progressivemail": {
"type": "textbox", "type": "textbox",
@@ -569,12 +354,7 @@
"text": "Progressive Mail" "text": "Progressive Mail"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 2
"label": {
"sticky": "w"
},
"default": 2
}
}, },
"halfmagic": { "halfmagic": {
"type": "textbox", "type": "textbox",
@@ -582,12 +362,7 @@
"text": "Half Magic" "text": "Half Magic"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
} }
}, },
"itemList4": { "itemList4": {
@@ -597,12 +372,7 @@
"text": "Quarter Magic" "text": "Quarter Magic"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"bombsplus5": { "bombsplus5": {
"type": "textbox", "type": "textbox",
@@ -610,12 +380,7 @@
"text": "Bomb Cap +5" "text": "Bomb Cap +5"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"bombsplus10": { "bombsplus10": {
"type": "textbox", "type": "textbox",
@@ -623,12 +388,7 @@
"text": "Bomb Cap +10" "text": "Bomb Cap +10"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"arrowsplus5": { "arrowsplus5": {
"type": "textbox", "type": "textbox",
@@ -636,12 +396,7 @@
"text": "Arrow Cap +5" "text": "Arrow Cap +5"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"arrowsplus10": { "arrowsplus10": {
"type": "textbox", "type": "textbox",
@@ -649,12 +404,7 @@
"text": "Arrow Cap +10" "text": "Arrow Cap +10"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"arrow1": { "arrow1": {
"type": "textbox", "type": "textbox",
@@ -662,12 +412,7 @@
"text": "Arrow (1)" "text": "Arrow (1)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"arrow10": { "arrow10": {
"type": "textbox", "type": "textbox",
@@ -675,12 +420,7 @@
"text": "Arrow (10)" "text": "Arrow (10)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 12
"label": {
"sticky": "w"
},
"default": 12
}
}, },
"bomb1": { "bomb1": {
"type": "textbox", "type": "textbox",
@@ -688,12 +428,7 @@
"text": "Bomb (1)" "text": "Bomb (1)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"bomb3": { "bomb3": {
"type": "textbox", "type": "textbox",
@@ -701,12 +436,7 @@
"text": "Bomb (3)" "text": "Bomb (3)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 16
"label": {
"sticky": "w"
},
"default": 16
}
}, },
"bomb10": { "bomb10": {
"type": "textbox", "type": "textbox",
@@ -714,12 +444,7 @@
"text": "Bomb (10)" "text": "Bomb (10)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
}, },
"rupee1": { "rupee1": {
"type": "textbox", "type": "textbox",
@@ -727,12 +452,7 @@
"text": "Rupee (1)" "text": "Rupee (1)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 2
"label": {
"sticky": "w"
},
"default": 2
}
}, },
"rupee5": { "rupee5": {
"type": "textbox", "type": "textbox",
@@ -740,12 +460,7 @@
"text": "Rupee (5)" "text": "Rupee (5)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 4
"label": {
"sticky": "w"
},
"default": 4
}
}, },
"rupee20": { "rupee20": {
"type": "textbox", "type": "textbox",
@@ -753,12 +468,7 @@
"text": "Rupee (20)" "text": "Rupee (20)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 28
"label": {
"sticky": "w"
},
"default": 28
}
}, },
"rupee50": { "rupee50": {
"type": "textbox", "type": "textbox",
@@ -766,12 +476,7 @@
"text": "Rupee (50)" "text": "Rupee (50)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 7
"label": {
"sticky": "w"
},
"default": 7
}
}, },
"rupee100": { "rupee100": {
"type": "textbox", "type": "textbox",
@@ -779,12 +484,7 @@
"text": "Rupee (100)" "text": "Rupee (100)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 1
"label": {
"sticky": "w"
},
"default": 1
}
} }
}, },
"itemList5": { "itemList5": {
@@ -794,12 +494,7 @@
"text": "Rupee (300)" "text": "Rupee (300)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 5
"label": {
"sticky": "w"
},
"default": 5
}
}, },
"blueclock": { "blueclock": {
"type": "textbox", "type": "textbox",
@@ -807,12 +502,7 @@
"text": "Blue Clock" "text": "Blue Clock"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"greenclock": { "greenclock": {
"type": "textbox", "type": "textbox",
@@ -820,12 +510,7 @@
"text": "Green Clock" "text": "Green Clock"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"redclock": { "redclock": {
"type": "textbox", "type": "textbox",
@@ -833,12 +518,7 @@
"text": "Red Clock" "text": "Red Clock"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"silversupgrade": { "silversupgrade": {
"type": "textbox", "type": "textbox",
@@ -846,12 +526,7 @@
"text": "Silver Arrows Upgrade" "text": "Silver Arrows Upgrade"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"generickeys": { "generickeys": {
"type": "textbox", "type": "textbox",
@@ -859,12 +534,7 @@
"text": "Generic Keys" "text": "Generic Keys"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"triforcepieces": { "triforcepieces": {
"type": "textbox", "type": "textbox",
@@ -872,12 +542,7 @@
"text": "Triforce Pieces" "text": "Triforce Pieces"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"triforcepiecesgoal": { "triforcepiecesgoal": {
"type": "textbox", "type": "textbox",
@@ -885,12 +550,7 @@
"text": "Triforce Pieces Goal" "text": "Triforce Pieces Goal"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"triforce": { "triforce": {
"type": "textbox", "type": "textbox",
@@ -898,12 +558,7 @@
"text": "Triforce (win game)" "text": "Triforce (win game)"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"rupoor": { "rupoor": {
"type": "textbox", "type": "textbox",
@@ -911,12 +566,7 @@
"text": "Rupoor" "text": "Rupoor"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 0
"label": {
"sticky": "w"
},
"default": 0
}
}, },
"rupoorcost": { "rupoorcost": {
"type": "textbox", "type": "textbox",
@@ -924,12 +574,7 @@
"text": "Rupoor Cost" "text": "Rupoor Cost"
}, },
"manager": "grid", "manager": "grid",
"managerAttrs": { "default": 10
"label": {
"sticky": "w"
},
"default": 10
}
} }
} }
} }
+271
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.createspoiler": "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"
}
}
@@ -1,26 +1,8 @@
{ {
"mapshuffle": { "keysanity": {
"type": "checkbox", "mapshuffle": { "type": "checkbox" },
"label": { "compassshuffle": { "type": "checkbox" },
"text": "Maps" "smallkeyshuffle": { "type": "checkbox" },
} "bigkeyshuffle": { "type": "checkbox" }
},
"compassshuffle": {
"type": "checkbox",
"label": {
"text": "Compasses"
}
},
"smallkeyshuffle": {
"type": "checkbox",
"label": {
"text": "Small Keys"
}
},
"bigkeyshuffle": {
"type": "checkbox",
"label": {
"text": "Big Keys"
}
} }
} }
@@ -1,49 +1,24 @@
{ {
"dungeondoorshuffle": { "widgets": {
"type": "selectbox", "dungeondoorshuffle": {
"label": { "type": "selectbox",
"text": "Dungeon Door Shuffle" "default": "basic",
"options": [
"vanilla",
"basic",
"crossed"
]
}, },
"managerAttrs": { "experimental": { "type": "checkbox" },
"label": { "dungeon_counters": {
"side": "left" "type": "selectbox",
}, "default": "default",
"selectbox": { "options": [
"side": "right" "default",
}, "off",
"default": "Basic" "on",
}, "pickup"
"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"
} }
} }
} }
@@ -1,93 +1,44 @@
{ {
"checkboxes": { "checkboxes": {
"potshuffle": { "potshuffle": { "type": "checkbox" }
"type": "checkbox",
"label": {
"text": "Pot Shuffle"
}
}
}, },
"leftEnemizerFrame": { "leftEnemizerFrame": {
"enemyshuffle": { "enemyshuffle": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Enemy Shuffle" "none",
}, "shuffled",
"managerAttrs": { "chaos"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "none",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
}, },
"bossshuffle": { "bossshuffle": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Boss Shuffle" "none",
}, "basic",
"managerAttrs": { "shuffled",
"label": { "chaos"
"side": "left" ]
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "none",
"Basic": "basic",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
} }
}, },
"rightEnemizerFrame": { "rightEnemizerFrame": {
"enemydamage": { "enemydamage": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Enemy Damage" "default",
}, "shuffled",
"managerAttrs": { "chaos"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "default",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
}, },
"enemyhealth": { "enemyhealth": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Enemy Health" "default",
}, "easy",
"managerAttrs": { "normal",
"label": { "hard",
"side": "left" "expert"
}, ]
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "default",
"Easy": "easy",
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
} }
} }
} }
@@ -1,40 +1,23 @@
{ {
"widgets": { "widgets": {
"openpyramid": { "openpyramid": { "type": "checkbox" },
"type": "checkbox", "shuffleganon": { "type": "checkbox" },
"label": { "entranceshuffle": {
"text": "Pre-open Pyramid Hole"
}
},
"shuffleganon": {
"type": "checkbox",
"label": {
"text": "Include Ganon's Tower and Pyramid Hole in shuffle pool"
}
},
"entranceshuffle": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Entrance Shuffle" "vanilla",
}, "simple",
"managerAttrs": { "restricted",
"label": { "side": "left" }, "full",
"selectbox": { "side": "right" } "crossed",
}, "insanity",
"options": { "restricted_legacy",
"Vanilla": "vanilla", "full_legacy",
"Simple": "simple", "madness_legacy",
"Restricted": "restricted", "insanity_legacy",
"Full": "full", "dungeonsfull",
"Crossed": "crossed", "dungeonssimple"
"Insanity": "insanity", ]
"Restricted (Legacy)": "restricted_legacy",
"Full (Legacy)": "full_legacy",
"Madness (Legacy)": "madness_legacy",
"Insanity (Legacy)": "insanity_legacy",
"Dungeons + Full": "dungeonsfull",
"Dungeons + Simple": "dungeonssimple"
}
} }
} }
} }
@@ -1,131 +1,63 @@
{ {
"checkboxes": { "checkboxes": {
"hints": { "hints": {
"type": "checkbox", "type": "checkbox",
"label": {
"text": "Include Helpful Hints"
},
"default": "true" "default": "true"
}, },
"nobgm": { "nobgm": { "type": "checkbox" },
"type": "checkbox", "quickswap": { "type": "checkbox" }
"label": {
"text": "Disable Music & MSU-1"
}
},
"quickswap": {
"type": "checkbox",
"label": {
"text": "L/R Quickswapping"
}
}
}, },
"leftRomOptionsFrame": { "leftRomOptionsFrame": {
"heartcolor": { "heartcolor": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Heart Color" "red",
}, "blue",
"managerAttrs": { "green",
"label": { "yellow",
"side": "left" "random"
}, ]
"selectbox": {
"side": "right"
}
},
"options": {
"Red": "red",
"Blue": "blue",
"Green": "green",
"Yellow": "yellow",
"Random": "random"
}
}, },
"heartbeep": { "heartbeep": {
"type": "selectbox", "type": "selectbox",
"label": { "default": "normal",
"text": "Heart Beep sound rate" "options": [
}, "double",
"managerAttrs": { "normal",
"label": { "half",
"side": "left" "quarter",
}, "off"
"selectbox": { ]
"side": "right"
},
"default": "Normal"
},
"options": {
"Double": "double",
"Normal": "normal",
"Half": "half",
"Quarter": "quarter",
"Off": "off"
}
} }
}, },
"rightRomOptionsFrame": { "rightRomOptionsFrame": {
"menuspeed": { "menuspeed": {
"type": "selectbox", "type": "selectbox",
"label": { "default": "normal",
"text": "Menu Speed" "options": [
}, "instant",
"managerAttrs": { "quadruple",
"label": { "triple",
"side": "left" "double",
}, "normal",
"selectbox": { "half"
"side": "right" ]
},
"default": "Normal"
},
"options": {
"Instant": "instant",
"Quadruple": "quadruple",
"Triple": "triple",
"Double": "double",
"Normal": "normal",
"Half": "half"
}
}, },
"owpalettes": { "owpalettes": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Overworld Palettes" "default",
}, "random",
"managerAttrs": { "blackout"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
}, },
"uwpalettes": { "uwpalettes": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Underworld Palettes" "default",
}, "random",
"managerAttrs": { "blackout"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
} }
} }
} }
@@ -1,45 +1,9 @@
{ {
"spoiler": { "checkboxes": {
"type": "checkbox", "createspoiler": { "type": "checkbox" },
"label": { "createrom": { "type": "checkbox" },
"text": "Create Spoiler Log" "calcplaythrough": { "type": "checkbox" },
} "usestartinventory": { "type": "checkbox" },
}, "usecustompool": { "type": "checkbox" }
"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"
}
} }
} }
@@ -0,0 +1,12 @@
{
"widgets": {
"saveonexit": {
"type": "selectbox",
"options": [
"ask",
"always",
"never"
]
}
}
}
+72 -222
View File
@@ -1,266 +1,116 @@
{ {
"checkboxes": { "checkboxes": {
"retro": { "retro": { "type": "checkbox" }
"type": "checkbox",
"label": {
"text": "Retro mode (universal keys)"
}
}
}, },
"leftItemFrame": { "leftItemFrame": {
"worldstate": { "worldstate": {
"type": "selectbox", "type": "selectbox",
"label": { "default": "open",
"text": "World State" "options": [
}, "standard",
"managerAttrs": { "open",
"label": { "inverted",
"side": "left" "retro"
}, ]
"selectbox": {
"side": "right"
},
"default": "Open"
},
"options": {
"Standard": "standard",
"Open": "open",
"Inverted": "inverted"
}
}, },
"logiclevel": { "logiclevel": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Logic Level" "noglitches",
}, "minorglitches",
"managerAttrs": { "nologic"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"No Glitches": "noglitches",
"Minor Glitches": "minorglitches",
"No Logic": "nologic"
}
}, },
"goal": { "goal": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Goal" "ganon",
}, "pedestal",
"managerAttrs": { "dungeons",
"label": { "triforcehunt",
"side": "left" "crystals"
}, ]
"selectbox": {
"side": "right"
}
},
"options": {
"Defeat Ganon": "ganon",
"Master Sword Pedestal": "pedestal",
"All Dungeons": "dungeons",
"Triforce Hunt": "triforcehunt",
"Crystals": "crystals"
}
}, },
"crystals_gt": { "crystals_gt": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Crystals to open GT" 0, 1, 2, 3, 4, 5, 6, 7,
}, "random"
"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"
}
}, },
"crystals_ganon": { "crystals_ganon": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Crystals to harm Ganon" 0, 1, 2, 3, 4, 5, 6, 7,
}, "random"
"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"
}
}, },
"weapons": { "weapons": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Weapons" "random",
}, "assured",
"managerAttrs": { "swordless",
"label": { "vanilla"
"side": "left" ]
},
"selectbox": {
"side": "right"
}
},
"options": {
"Randomized": "random",
"Assured": "assured",
"Swordless": "swordless",
"Vanilla": "vanilla"
}
} }
}, },
"rightItemFrame": { "rightItemFrame": {
"itempool": { "itempool": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Item Pool" "normal",
}, "hard",
"managerAttrs": { "expert"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
}, },
"itemfunction": { "itemfunction": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Item Functionality" "normal",
}, "hard",
"managerAttrs": { "expert"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
}, },
"timer": { "timer": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Timer Setting" "none",
}, "display",
"managerAttrs": { "timed",
"label": { "timed-ohko",
"side": "left" "ohko",
}, "timed-countdown"
"selectbox": { ]
"side": "right"
}
},
"options": {
"No Timer": "none",
"Stopwatch": "display",
"Timed": "timed",
"Timed OHKO": "timed-ohko",
"OHKO": "ohko",
"Timed Countdown": "timed-countdown"
}
}, },
"progressives": { "progressives": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Progressive Items" "on",
}, "off",
"managerAttrs": { "random"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"On": "on",
"Off": "off",
"Random": "random"
}
}, },
"accessibility": { "accessibility": {
"type": "selectbox", "type": "selectbox",
"label": { "options": [
"text": "Accessibility" "items",
}, "locations",
"managerAttrs": { "none"
"label": { ]
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"100% Inventory": "items",
"100% Locations": "locations",
"Beatable": "none"
}
}, },
"sortingalgo": { "sortingalgo": {
"type": "selectbox", "type": "selectbox",
"label": { "default": "balanced",
"text": "Item Sorting" "options": [
}, "freshness",
"managerAttrs": { "flood",
"label": { "vt21",
"side": "left" "vt22",
}, "vt25",
"selectbox": { "vt26",
"side": "right" "balanced"
}, ]
"default": "Balanced"
},
"options": {
"Freshness": "freshness",
"Flood": "flood",
"VT8.21": "vt21",
"VT8.22": "vt22",
"VT8.25": "vt25",
"VT8.26": "vt26",
"Balanced": "balanced"
}
} }
} }
} }
@@ -1,16 +1,5 @@
{ {
"worlds": { "widgets": {
"type": "spinbox", "worlds": { "type": "spinbox" }
"label": {
"text": "Worlds"
},
"managerAttrs": {
"label": {
"side": "left"
},
"spinbox": {
"side": "right"
}
}
} }
} }
+3
View File
@@ -0,0 +1,3 @@
{
}
@@ -0,0 +1 @@
aenum
+1
View File
@@ -0,0 +1 @@
#do nothing, just exist to make "resources.ci" package
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "resources.ci.common" package
+137
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 = os.path.join(".","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)
+42
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(os.path.join(".","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(os.path.join(".",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.path.join("."))
os.rename(os.path.join(".",UPX_SLUG),os.path.join(".","upx"))
os.remove(os.path.join(".",UPX_FILE))
print("UPX should " + ("not " if not os.path.isdir(os.path.join(".","upx")) else "") + "be available.")
+29
View File
@@ -0,0 +1,29 @@
import subprocess # do stuff at the shell level
import os
def git_clean(clean_ignored=True, clean_user=False):
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
"EnemizerCLI" # keep EnemizerCLI files
]
if not clean_user:
excludes.append(os.path.join("resources","user*")) # keep user resources
excludes = ['--exclude={0}'.format(exclude) for exclude in excludes]
# d: directories, f: files, x: ignored files
switches = "df" + ("x" if clean_ignored else "")
# clean the git slate
subprocess.check_call([
"git", # run a git command
"clean", # clean command
"-" + switches,
*excludes])
if __name__ == "__main__":
git_clean()
+3
View File
@@ -0,0 +1,3 @@
from git_clean import git_clean
git_clean(clean_user=True)
+27
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"])
+20
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")
)
+40
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)
)
+127
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
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "user" folder
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source" package
+112
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
View File
@@ -0,0 +1,3 @@
# Need a dummy class
class Empty():
pass
@@ -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 from glob import glob
import json import json
import os import os
@@ -32,10 +32,13 @@ class SpriteSelector(object):
webbrowser.open("http://alttpr.com/sprite_preview") webbrowser.open("http://alttpr.com/sprite_preview")
def open_unofficial_sprite_dir(_evt): def open_unofficial_sprite_dir(_evt):
if not os.path.isdir(self.unofficial_sprite_dir):
os.makedirs(self.unofficial_sprite_dir)
open_file(self.unofficial_sprite_dir) open_file(self.unofficial_sprite_dir)
# Open SpriteSomething directory for Link sprites
def open_spritesomething_listing(_evt): 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_frametitle = Frame(self.window)
official_title_text = Label(official_frametitle, text="Official Sprites") official_title_text = Label(official_frametitle, text="Official Sprites")
@@ -50,12 +53,13 @@ class SpriteSelector(object):
unofficial_title_text.pack(side=LEFT) unofficial_title_text.pack(side=LEFT)
unofficial_title_link.pack(side=LEFT) unofficial_title_link.pack(side=LEFT)
unofficial_title_link.bind("<Button-1>", open_unofficial_sprite_dir) 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 = Label(unofficial_frametitle, text="(SpriteSomething)", fg="blue", cursor="hand2")
spritesomething_title_link.pack(side=LEFT) spritesomething_title_link.pack(side=LEFT)
spritesomething_title_link.bind("<Button-1>", open_spritesomething_listing) spritesomething_title_link.bind("<Button-1>", open_spritesomething_listing)
self.icon_section(official_frametitle, self.official_sprite_dir+'/*', 'Official sprites not found. Click "Update official sprites" to download them.') self.icon_section(official_frametitle, os.path.join(self.official_sprite_dir,"*"), 'Official sprites not found. Click "Update official sprites" to download them.')
self.icon_section(unofficial_frametitle, self.unofficial_sprite_dir+'/*', 'Put sprites in the unofficial sprites folder (see open link above) to have them appear here.') self.icon_section(unofficial_frametitle, os.path.join(self.unofficial_sprite_dir,"*"), 'Put sprites in the unofficial sprites folder (see open link above) to have them appear here.')
frame = Frame(self.window) frame = Frame(self.window)
frame.pack(side=BOTTOM, fill=X, pady=5) frame.pack(side=BOTTOM, fill=X, pady=5)
@@ -147,10 +151,10 @@ class SpriteSelector(object):
try: try:
task.update_status("Determining needed sprites") task.update_status("Determining needed sprites")
current_sprites = [os.path.basename(file) for file in glob(self.official_sprite_dir+'/*')] current_sprites = [os.path.basename(file) for file in glob(os.path.join(self.official_sprite_dir,"*"))]
official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr] official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites] needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites]
bundled_sprites = [os.path.basename(file) for file in glob(self.local_official_sprite_dir+'/*')] bundled_sprites = [os.path.basename(file) for file in glob(os.path.join(self.unofficial_sprite_dir,"*"))]
# todo: eventually use the above list to avoid downloading any sprites that we already have cached in the bundle. # todo: eventually use the above list to avoid downloading any sprites that we already have cached in the bundle.
official_filenames = [filename for (_, filename) in official_sprites] official_filenames = [filename for (_, filename) in official_sprites]
@@ -227,23 +231,23 @@ class SpriteSelector(object):
@property @property
def official_sprite_dir(self): def official_sprite_dir(self):
if is_bundled(): # if is_bundled():
return output_path("sprites/official") # return output_path(os.path.join("sprites","official"))
return self.local_official_sprite_dir return self.local_official_sprite_dir
@property @property
def local_official_sprite_dir(self): def local_official_sprite_dir(self):
return local_path("data/sprites/official") return local_path(os.path.join("data","sprites","official"))
@property @property
def unofficial_sprite_dir(self): def unofficial_sprite_dir(self):
if is_bundled(): # if is_bundled():
return output_path("sprites/unofficial") # return output_path(os.path.join("sprites","unofficial"))
return self.local_unofficial_sprite_dir return self.local_unofficial_sprite_dir
@property @property
def local_unofficial_sprite_dir(self): def local_unofficial_sprite_dir(self):
return local_path("data/sprites/unofficial") return local_path(os.path.join("data","sprites","unofficial"))
def get_image_for_sprite(sprite): def get_image_for_sprite(sprite):
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source.classes" package
@@ -1,3 +1,4 @@
# Ordered list of items in Custom Item Pool page and Starting Inventory page
CUSTOMITEMS = [ CUSTOMITEMS = [
"bow", "progressivebow", "boomerang", "redmerang", "hookshot", "bow", "progressivebow", "boomerang", "redmerang", "hookshot",
"mushroom", "powder", "firerod", "icerod", "bombos", "mushroom", "powder", "firerod", "icerod", "bombos",
@@ -20,11 +21,13 @@ CUSTOMITEMS = [
"rupoorcost" "rupoorcost"
] ]
# These can't be in the Starting Inventory page
CANTSTARTWITH = [ CANTSTARTWITH = [
"triforcepiecesgoal", "triforce", "rupoor", "triforcepiecesgoal", "triforce", "rupoor",
"rupoorcost" "rupoorcost"
] ]
# In the same order as CUSTOMITEMS, these are Pretty Labels for each option
CUSTOMITEMLABELS = [ CUSTOMITEMLABELS = [
"Bow", "Progressive Bow", "Blue Boomerang", "Red Boomerang", "Hookshot", "Bow", "Progressive Bow", "Blue Boomerang", "Red Boomerang", "Hookshot",
"Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos", "Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos",
@@ -33,7 +36,7 @@ CUSTOMITEMLABELS = [
"Ocarina", "Bug Catching Net", "Book of Mudora", "Bottle", "Cane of Somaria", "Ocarina", "Bug Catching Net", "Book of Mudora", "Bottle", "Cane of Somaria",
"Cane of Byrna", "Magic Cape", "Magic Mirror", "Pegasus Boots", "Power Glove", "Cane of Byrna", "Magic Cape", "Magic Mirror", "Pegasus Boots", "Power Glove",
"Titans Mitts", "Progressive Glove", "Flippers", "Moon Pearl", "Piece of Heart", "Titans Mitts", "Progressive Glove", "Flippers", "Moon Pearl", "Piece of Heart",
"Boss Heart Container", "Sanctuary Heart Container", "Fighter Sword", "Master Sword", "Tempered Sword", "Boss Heart Container", "Sanctuary Heart Container", "Fighter Sword", "Master Sword", "Tempered Sword",
"Golden Sword", "Progressive Sword", "Blue Shield", "Red Shield", "Mirror Shield", "Golden Sword", "Progressive Sword", "Blue Shield", "Red Shield", "Mirror Shield",
"Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)", "Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)",
@@ -47,6 +50,8 @@ CUSTOMITEMLABELS = [
"Rupoor Cost" "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 = { SETTINGSTOPROCESS = {
"randomizer": { "randomizer": {
"item": { "item": {
@@ -85,9 +90,6 @@ SETTINGSTOPROCESS = {
"experimental": "experimental", "experimental": "experimental",
"dungeon_counters": "dungeon_counters" "dungeon_counters": "dungeon_counters"
}, },
"multiworld": {
"names": "names"
},
"gameoptions": { "gameoptions": {
"hints": "hints", "hints": "hints",
"nobgm": "disablemusic", "nobgm": "disablemusic",
@@ -99,11 +101,19 @@ SETTINGSTOPROCESS = {
"uwpalettes": "uw_palettes" "uwpalettes": "uw_palettes"
}, },
"generation": { "generation": {
"spoiler": "create_spoiler", "createspoiler": "create_spoiler",
"suppressrom": "suppress_rom", "createrom": "create_rom",
"calcplaythrough": "calc_playthrough",
"usestartinventory": "usestartinventory", "usestartinventory": "usestartinventory",
"usecustompool": "custom", "usecustompool": "custom",
"saveonexit": "saveonexit" "saveonexit": "saveonexit"
} }
},
"bottom": {
"content": {
"names": "names",
"seed": "seed",
"generationcount": "count"
}
} }
} }
+47
View File
@@ -0,0 +1,47 @@
import platform, sys, os, subprocess
import pkg_resources
from datetime import datetime
def diagpad(str):
return str.ljust(len("ALttP Door Randomizer Version") + 5,'.')
def output(APP_VERSION):
lines = [
"ALttP Door Randomizer Diagnostics",
"=================================",
diagpad("UTC Time") + str(datetime.utcnow())[:19],
diagpad("ALttP Door Randomizer Version") + APP_VERSION,
diagpad("Python Version") + platform.python_version()
]
lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release()))
if hasattr(sys, "executable"):
lines.append(diagpad("Executable") + sys.executable)
lines.append(diagpad("Build Date") + platform.python_build()[1])
lines.append(diagpad("Compiler") + platform.python_compiler())
if hasattr(sys, "api_version"):
lines.append(diagpad("Python API") + str(sys.api_version))
if hasattr(os, "sep"):
lines.append(diagpad("Filepath Separator") + os.sep)
if hasattr(os, "pathsep"):
lines.append(diagpad("Path Env Separator") + os.pathsep)
lines.append("")
lines.append("Packages")
lines.append("--------")
'''
#this breaks when run from the .exe
reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'])
installed_packages = [r.decode() for r in reqs.split()]
for pkg in installed_packages:
pkg = pkg.split("==")
lines.append(diagpad(pkg[0]) + pkg[1])
'''
installed_packages = [str(d) for d in pkg_resources.working_set] #this doesn't work from the .exe either, but it doesn't crash the program
installed_packages.sort()
for pkg in installed_packages:
pkg = pkg.split(' ')
lines.append(diagpad(pkg[0]) + pkg[1])
return lines
if __name__ == "__main__":
raise AssertionError(f"Called main() on utility library {__file__}")
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source.gui" package
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source.gui.about" package
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source.gui.adjust" package
@@ -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 AdjusterMain import adjust
from argparse import Namespace from argparse import Namespace
from classes.SpriteSelector import SpriteSelector from source.classes.SpriteSelector import SpriteSelector
import gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import logging import logging
import os import os
@@ -19,6 +19,7 @@ def adjust_page(top, parent, settings):
self.frames["checkboxes"] = Frame(self) self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W) self.frames["checkboxes"].pack(anchor=W)
# Adjust option frames
self.frames["selectOptionsFrame"] = Frame(self) self.frames["selectOptionsFrame"] = Frame(self)
self.frames["leftAdjustFrame"] = Frame(self.frames["selectOptionsFrame"]) self.frames["leftAdjustFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["rightAdjustFrame"] = 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["rightAdjustFrame"].pack(side=RIGHT)
self.frames["bottomAdjustFrame"].pack(fill=X) 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: with open(os.path.join("resources","app","gui","adjust","overview","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns) myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items(): for framename,theseWidgets in myDict.items():
@@ -40,6 +43,7 @@ def adjust_page(top, parent, settings):
self.widgets[key].pack(packAttrs) self.widgets[key].pack(packAttrs)
# Sprite Selection # Sprite Selection
# This one's more-complicated, build it and stuff it
self.spriteNameVar2 = StringVar() self.spriteNameVar2 = StringVar()
spriteDialogFrame2 = Frame(self.frames["leftAdjustFrame"]) spriteDialogFrame2 = Frame(self.frames["leftAdjustFrame"])
baseSpriteLabel2 = Label(spriteDialogFrame2, text='Sprite:') baseSpriteLabel2 = Label(spriteDialogFrame2, text='Sprite:')
@@ -65,6 +69,8 @@ def adjust_page(top, parent, settings):
spriteSelectButton2.pack(side=LEFT) spriteSelectButton2.pack(side=LEFT)
spriteDialogFrame2.pack(anchor=E) 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"]) adjustRomFrame = Frame(self.frames["bottomAdjustFrame"])
adjustRomLabel = Label(adjustRomFrame, text='Rom to adjust: ') adjustRomLabel = Label(adjustRomFrame, text='Rom to adjust: ')
self.romVar2 = StringVar(value=settings["rom"]) self.romVar2 = StringVar(value=settings["rom"])
@@ -82,6 +88,7 @@ def adjust_page(top, parent, settings):
romSelectButton2.pack(side=LEFT) romSelectButton2.pack(side=LEFT)
adjustRomFrame.pack(fill=X) adjustRomFrame.pack(fill=X)
# These are the options to Adjust
def adjustRom(): def adjustRom():
options = { options = {
"heartbeep": "heartbeep", "heartbeep": "heartbeep",
@@ -97,7 +104,7 @@ def adjust_page(top, parent, settings):
arg = options[option] arg = options[option]
setattr(guiargs, arg, self.widgets[option].storageVar.get()) setattr(guiargs, arg, self.widgets[option].storageVar.get())
guiargs.rom = self.romVar2.get() guiargs.rom = self.romVar2.get()
guiargs.baserom = top.pages["randomizer"].pages["generation"].romVar.get() guiargs.baserom = top.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
guiargs.sprite = self.sprite guiargs.sprite = self.sprite
try: try:
adjust(args=guiargs) adjust(args=guiargs)
+275
View File
@@ -0,0 +1,275 @@
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)
made["enemizer"] = parent.fish.translate("cli","cli","used.enemizer")
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"
successMsg += (made["enemizer"] % (YES if needEnemizer 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
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source.gui.custom" package
@@ -1,25 +1,27 @@
from tkinter import ttk, Frame, N, LEFT, VERTICAL, Y from tkinter import ttk, Frame, N, E, W, LEFT, X, VERTICAL, Y
import gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os 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 # Custom Item Pool
self = ttk.Frame(parent) self = ttk.Frame(parent)
# Create uniform list columns
def create_list_frame(parent, framename): def create_list_frame(parent, framename):
parent.frames[framename] = Frame(parent) parent.frames[framename] = Frame(parent)
parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N) parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N)
parent.frames[framename].thisRow = 0 parent.frames[framename].thisRow = 0
parent.frames[framename].thisCol = 0 parent.frames[framename].thisCol = 0
# Create a vertical rule to help with splitting columns visually
def create_vertical_rule(num=1): 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) 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): def validation(P):
if str.isdigit(P) or P == "": if str.isdigit(P) or P == "":
return True return True
@@ -32,6 +34,7 @@ def custom_page(top, parent):
# Custom Item Pool option sections # Custom Item Pool option sections
self.frames = {} self.frames = {}
# Create 5 columns with 2 vertical rules in between each
create_list_frame(self, "itemList1") create_list_frame(self, "itemList1")
create_vertical_rule(2) create_vertical_rule(2)
create_list_frame(self, "itemList2") create_list_frame(self, "itemList2")
@@ -42,6 +45,8 @@ def custom_page(top, parent):
create_vertical_rule(2) create_vertical_rule(2)
create_list_frame(self, "itemList5") 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: with open(os.path.join("resources", "app", "gui", "custom", "overview", "widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns) myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items(): for framename,theseWidgets in myDict.items():
@@ -49,6 +54,7 @@ def custom_page(top, parent):
for key in dictWidgets: for key in dictWidgets:
self.customWidgets[key] = dictWidgets[key] self.customWidgets[key] = dictWidgets[key]
# Load Custom Item Pool settings from settings file
for key in CONST.CUSTOMITEMS: for key in CONST.CUSTOMITEMS:
self.customWidgets[key].storageVar.set(top.settings["customitemarray"][key]) self.customWidgets[key].storageVar.set(top.settings["customitemarray"][key])
+208
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])
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "source.gui.randomize" package
@@ -1,5 +1,5 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT from tkinter import ttk, Frame, Label, E, W, LEFT, RIGHT
import gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os import os
@@ -19,17 +19,23 @@ def dungeon_page(parent):
mscbLabel = Label(self.frames["keysanity"], text="Shuffle: ") mscbLabel = Label(self.frames["keysanity"], text="Shuffle: ")
mscbLabel.pack(side=LEFT) 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: with open(os.path.join("resources","app","gui","randomize","dungeon","keysanity.json")) as keysanityItems:
myDict = json.load(keysanityItems) myDict = json.load(keysanityItems)
myDict = myDict["keysanity"]
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"]) dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"])
for key in dictWidgets: for key in dictWidgets:
self.widgets[key] = dictWidgets[key] self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(side=LEFT) self.widgets[key].pack(side=LEFT)
# These get split left & right
self.frames["widgets"] = Frame(self) self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W) self.frames["widgets"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","dungeon","widgets.json")) as dungeonWidgets: with open(os.path.join("resources","app","gui","randomize","dungeon","widgets.json")) as dungeonWidgets:
myDict = json.load(dungeonWidgets) myDict = json.load(dungeonWidgets)
myDict = myDict["widgets"]
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"]) dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"])
for key in dictWidgets: for key in dictWidgets:
self.widgets[key] = dictWidgets[key] self.widgets[key] = dictWidgets[key]
+89
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
@@ -1,5 +1,5 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT from tkinter import ttk, Frame, E, W, LEFT, RIGHT
import gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os import os
@@ -15,6 +15,11 @@ def entrando_page(parent):
self.frames["widgets"] = Frame(self) self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W) 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: with open(os.path.join("resources","app","gui","randomize","entrando","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns) myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items(): for framename,theseWidgets in myDict.items():
@@ -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 from functools import partial
import classes.SpriteSelector as spriteSelector import source.classes.SpriteSelector as spriteSelector
import gui.widgets as widgets import source.gui.widgets as widgets
import json import json
import os import os
@@ -17,11 +17,17 @@ def gameoptions_page(top, parent):
self.frames["checkboxes"] = Frame(self) self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W) self.frames["checkboxes"].pack(anchor=W)
# Game Options frames
self.frames["leftRomOptionsFrame"] = Frame(self) self.frames["leftRomOptionsFrame"] = Frame(self)
self.frames["rightRomOptionsFrame"] = Frame(self) self.frames["rightRomOptionsFrame"] = Frame(self)
self.frames["leftRomOptionsFrame"].pack(side=LEFT) self.frames["leftRomOptionsFrame"].pack(side=LEFT)
self.frames["rightRomOptionsFrame"].pack(side=RIGHT) 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: with open(os.path.join("resources","app","gui","randomize","gameoptions","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns) myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items(): for framename,theseWidgets in myDict.items():
@@ -34,6 +40,7 @@ def gameoptions_page(top, parent):
self.widgets[key].pack(packAttrs) self.widgets[key].pack(packAttrs)
## Sprite selection ## Sprite selection
# This one's more-complicated, build it and stuff it
spriteDialogFrame = Frame(self.frames["leftRomOptionsFrame"]) spriteDialogFrame = Frame(self.frames["leftRomOptionsFrame"])
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:') 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) spriteNameVar.set(sprite_param.name)
if randomSpriteVar: if randomSpriteVar:
randomSpriteVar.set(random_sprite) randomSpriteVar.set(random_sprite)
+79
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

Some files were not shown because too many files have changed in this diff Show More