From ff678659f94f389eaf2f4bf599f0017ea8046742 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Mon, 23 Mar 2020 21:39:45 -0700 Subject: [PATCH 01/11] Add Suppress Spoiler --- CLI.py | 1 + Utils.py | 9 +++++++++ resources/app/cli/args.json | 7 ++++++- resources/app/gui/lang/en.json | 2 +- resources/app/gui/randomize/generation/checkboxes.json | 2 +- source/classes/constants.py | 2 +- 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CLI.py b/CLI.py index eec8327a..dabc7ed1 100644 --- a/CLI.py +++ b/CLI.py @@ -156,6 +156,7 @@ def parse_settings(): "ow_palettes": "default", "uw_palettes": "default", + "suppress_spoiler": True, "create_spoiler": False, "skip_playthrough": False, "calc_playthrough": True, diff --git a/Utils.py b/Utils.py index 63670d78..1735dfe6 100644 --- a/Utils.py +++ b/Utils.py @@ -251,6 +251,15 @@ def print_wiki_doors_by_region(d_regions, world, player): def update_deprecated_args(args): argVars = vars(args) truthy = [ 1, True, "True", "true" ] + # Don't do: Yes + # Do: No + if "suppress_spoiler" in argVars: + args.create_spoiler = args.suppress_spoiler not in truthy + # 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_rom" in argVars: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index cc639ab5..38cbba8c 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -1,9 +1,14 @@ { "lang": {}, "create_spoiler": { - "action": "store_true", + "action": "store_false", "type": "bool" }, + "suppress_spoiler": { + "action": "store_true", + "dest": "create_spoiler", + "help": "suppress" + }, "logic": { "choices": [ "noglitches", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index da499b03..0ea37616 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -154,7 +154,7 @@ "randomizer.gameoptions.sprite.unchanged": "(unchanged)", - "randomizer.generation.spoiler": "Create Spoiler Log", + "randomizer.generation.createspoiler": "Create Spoiler Log", "randomizer.generation.createrom": "Create Patched ROM", "randomizer.generation.calcplaythrough": "Calculate Playthrough", "randomizer.generation.usestartinventory": "Use Starting Inventory", diff --git a/resources/app/gui/randomize/generation/checkboxes.json b/resources/app/gui/randomize/generation/checkboxes.json index db020e6d..bb0ef016 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -1,6 +1,6 @@ { "checkboxes": { - "spoiler": { "type": "checkbox" }, + "createspoiler": { "type": "checkbox" }, "createrom": { "type": "checkbox" }, "calcplaythrough": { "type": "checkbox" }, "usestartinventory": { "type": "checkbox" }, diff --git a/source/classes/constants.py b/source/classes/constants.py index e8145f66..1b235800 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -101,7 +101,7 @@ SETTINGSTOPROCESS = { "uwpalettes": "uw_palettes" }, "generation": { - "spoiler": "create_spoiler", + "createspoiler": "create_spoiler", "createrom": "create_rom", "calcplaythrough": "calc_playthrough", "usestartinventory": "usestartinventory", From 406806a70b58b9d5bc9d0055b524865cc6da0956 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Mon, 23 Mar 2020 21:41:09 -0700 Subject: [PATCH 02/11] Outfilename shenanigans --- Main.py | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/Main.py b/Main.py index 5085c935..b55b8918 100644 --- a/Main.py +++ b/Main.py @@ -242,13 +242,44 @@ def main(args, seed=None, fish=None): outfilepname += f'_P{player}' 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 '' - outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player], - world.mode[player], world.goal[player], - "" if world.timer in ['none', 'display'] else "-" + world.timer, - world.shuffle[player], world.doorShuffle[player], world.algorithm, mcsb_name, - "-retro" if world.retro[player] else "", - "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", - "-nohints" if not world.hints[player] else "")) if not args.outputname else '' + outfilestuffs = { + "logic": world.logic[player], # 0 + "difficulty": world.difficulty[player], # 1 + "difficulty_adjustments": world.difficulty_adjustments[player], # 2 + "mode": world.mode[player], # 3 + "goal": world.goal[player], # 4 + "timer": str(world.timer[player]), # 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[player], # B + "hints": str(world.hints[player]) # 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')) if world.players > 1: From 871278a17a14c149d28d369bf92f7162a94a9876 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Mon, 23 Mar 2020 21:50:37 -0700 Subject: [PATCH 03/11] Restrict AutoRelease --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff6d8368..ca377868 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,6 +214,7 @@ jobs: 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 @@ -225,6 +226,7 @@ jobs: 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 @@ -236,6 +238,7 @@ jobs: 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 @@ -247,3 +250,4 @@ jobs: 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') From 7aa6195bf0bad9f062abd5e08426c309f9eec583 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Tue, 24 Mar 2020 00:44:03 -0700 Subject: [PATCH 04/11] Add Diags to CLI --- DungeonRandomizer.py | 10 +++++++- resources/app/cli/args.json | 4 ++++ source/classes/diags.py | 47 +++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 source/classes/diags.py diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index 15e73f85..4723b8db 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -9,9 +9,10 @@ import shlex import sys from source.classes.BabelFish import BabelFish +import source.classes.diags as diagnostics from CLI import parse_cli, get_args_priority -from Main import main, EnemizerError +from Main import main, EnemizerError, __version__ from Rom import get_sprite_from_name from Utils import is_bundled, close_console from Fill import FillError @@ -19,6 +20,13 @@ from Fill import FillError def start(): 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: # for the bundled builds, if we have no arguments, the user # probably wants the gui. Users of the bundled build who want the command line diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 38cbba8c..e74e1736 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -1,5 +1,9 @@ { "lang": {}, + "diags": { + "action": "store_true", + "type": "bool" + }, "create_spoiler": { "action": "store_false", "type": "bool" diff --git a/source/classes/diags.py b/source/classes/diags.py new file mode 100644 index 00000000..3e2c4121 --- /dev/null +++ b/source/classes/diags.py @@ -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__}") From 01a1190b5860deb265ccfd0d06e3c57695e429c3 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Tue, 24 Mar 2020 01:26:14 -0700 Subject: [PATCH 05/11] Fix bugs --- Main.py | 4 ++-- source/gui/adjust/overview.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Main.py b/Main.py index b55b8918..dfdbd5e1 100644 --- a/Main.py +++ b/Main.py @@ -248,13 +248,13 @@ def main(args, seed=None, fish=None): "difficulty_adjustments": world.difficulty_adjustments[player], # 2 "mode": world.mode[player], # 3 "goal": world.goal[player], # 4 - "timer": str(world.timer[player]), # 5 + "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[player], # B + "progressive": world.progressive, # B "hints": str(world.hints[player]) # C } # 0 1 2 3 4 5 6 7 8 9 A B C diff --git a/source/gui/adjust/overview.py b/source/gui/adjust/overview.py index c524d02e..4f7fe543 100644 --- a/source/gui/adjust/overview.py +++ b/source/gui/adjust/overview.py @@ -104,7 +104,7 @@ def adjust_page(top, parent, settings): arg = options[option] setattr(guiargs, arg, self.widgets[option].storageVar.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 try: adjust(args=guiargs) From 6c484745a2a64fa39c6b940faafbd78e7faa104d Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Tue, 24 Mar 2020 01:35:14 -0700 Subject: [PATCH 06/11] Keep EnemizerCLI --- resources/ci/common/git_clean.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/ci/common/git_clean.py b/resources/ci/common/git_clean.py index 7c2e2cfa..ecef5507 100644 --- a/resources/ci/common/git_clean.py +++ b/resources/ci/common/git_clean.py @@ -5,7 +5,8 @@ def git_clean(): ".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 + "*app*version.*", # keep appversion files + "EnemizerCLI" # keep EnemizerCLI ] excludes = ['--exclude={0}'.format(exclude) for exclude in excludes] From 599985e4b184e92fbdb7604c84dad1b873113742 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Sat, 28 Mar 2020 19:03:08 -0700 Subject: [PATCH 07/11] Use os.path more Add Enemizer note --- DungeonRandomizer.spec | 5 ++++- Gui.spec | 4 +++- GuiUtils.py | 6 +++--- Main.py | 3 ++- Rom.py | 4 ++-- build-dr.py | 5 ++++- build-gui.py | 5 ++++- data/default.zspr | Bin 28882 -> 0 bytes requirements.txt | 3 --- resources/app/cli/lang/en.json | 1 + resources/ci/common/common.py | 2 +- resources/ci/common/get_upx.py | 12 ++++++------ resources/ci/common/git_clean.py | 14 +++++++++++--- resources/ci/common/git_cleanest.py | 3 +++ source/classes/SpriteSelector.py | 25 +++++++++++++------------ source/gui/bottom.py | 2 ++ 16 files changed, 59 insertions(+), 35 deletions(-) delete mode 100644 data/default.zspr delete mode 100644 requirements.txt create mode 100644 resources/ci/common/git_cleanest.py diff --git a/DungeonRandomizer.spec b/DungeonRandomizer.spec index 163e8413..1ef92104 100644 --- a/DungeonRandomizer.spec +++ b/DungeonRandomizer.spec @@ -5,6 +5,8 @@ import sys block_cipher = None console = True +BINARY_SLUG = "DungeonRandomizer" + def recurse_for_py_files(names_so_far): returnvalue = [] for name in os.listdir(os.path.join(*names_so_far)): @@ -29,6 +31,7 @@ binaries = [] # binaries.append(("ucrtbase.dll",".")) a = Analysis(['DungeonRandomizer.py'], +a = Analysis([f"../{BINARY_SLUG}.py"], pathex=[], binaries=binaries, datas=[], @@ -56,7 +59,7 @@ exe = EXE(pyz, a.zipfiles, a.datas, [], - name='DungeonRandomizer', + name=BINARY_SLUG, debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/Gui.spec b/Gui.spec index a1b1a86c..0ccabada 100644 --- a/Gui.spec +++ b/Gui.spec @@ -7,6 +7,7 @@ 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): returnvalue = [] @@ -32,6 +33,7 @@ binaries = [] # binaries.append(("ucrtbase.dll",".")) a = Analysis(['DungeonRandomizer.py'], +a = Analysis([f"../{BINARY_SLUG}.py"], pathex=[], binaries=binaries, datas=[], @@ -59,7 +61,7 @@ exe = EXE(pyz, a.zipfiles, a.datas, [], - name='Gui', + name=BINARY_SLUG, debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/GuiUtils.py b/GuiUtils.py index e9361632..f0aee3b9 100644 --- a/GuiUtils.py +++ b/GuiUtils.py @@ -5,9 +5,9 @@ import tkinter as tk from Utils import local_path def set_icon(window): - er16 = tk.PhotoImage(file=local_path('data/ER16.gif')) - er32 = tk.PhotoImage(file=local_path('data/ER32.gif')) - er48 = tk.PhotoImage(file=local_path('data/ER32.gif')) + er16 = tk.PhotoImage(file=local_path(os.path.join("data","ER16.gif"))) + er32 = tk.PhotoImage(file=local_path(os.path.join("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 # Although tkinter is intended to be thread safe, there are many reports of issues diff --git a/Main.py b/Main.py index dfdbd5e1..0cac21e5 100644 --- a/Main.py +++ b/Main.py @@ -186,12 +186,12 @@ def main(args, seed=None, fish=None): rom_names = [] jsonout = {} + enemized = False if not args.suppress_rom: logger.info(world.fish.translate("cli","cli","patching.rom")) for team in range(world.teams): for player in range(1, world.players + 1): sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit' - enemized = False use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none' or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or args.shufflepots[player] or sprite_random_on_hit) @@ -313,6 +313,7 @@ def main(args, seed=None, fish=None): 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) diff --git a/Rom.py b/Rom.py index db84aefd..5f204f38 100644 --- a/Rom.py +++ b/Rom.py @@ -162,7 +162,7 @@ def read_rom(stream): def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit): 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") randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json')) options_path = os.path.abspath(output_path('enemizer_options.json')) @@ -305,7 +305,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r _sprite_table = {} def _populate_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): filepath = os.path.join(dir, file) if not os.path.isfile(filepath): diff --git a/build-dr.py b/build-dr.py index 08c1ce93..46f5b88a 100644 --- a/build-dr.py +++ b/build-dr.py @@ -3,6 +3,9 @@ import os import shutil import sys +# Spec file +SPEC_FILE = os.path.join("DungeonRandomizer.spec") + # Destination is current dir DEST_DIRECTORY = '.' @@ -16,7 +19,7 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform. shutil.rmtree("build") # Run pyinstaller for DungeonRandomizer -subprocess.run(" ".join(["pyinstaller DungeonRandomizer.spec ", +subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", upx_string, "-y ", "--onefile ", diff --git a/build-gui.py b/build-gui.py index ff8ccc90..6cc69228 100644 --- a/build-gui.py +++ b/build-gui.py @@ -3,6 +3,9 @@ import os import shutil import sys +# Spec file +SPEC_FILE = os.path.join("Gui.spec") + # Destination is current dir DEST_DIRECTORY = '.' @@ -16,7 +19,7 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform. shutil.rmtree("build") # Run pyinstaller for Gui -subprocess.run(" ".join(["pyinstaller Gui.spec ", +subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", upx_string, "-y ", "--onefile ", diff --git a/data/default.zspr b/data/default.zspr deleted file mode 100644 index fa0c9836a67c39ca00cb7515585e00ec36219982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28882 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 MtLpA6_dMeIA2ZywzW@LL diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 061dd9fe..00000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -aioconsole==0.1.15 -colorama==0.4.3 -websockets==8.1 diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 6edf5496..06830db5 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -40,6 +40,7 @@ "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", diff --git a/resources/ci/common/common.py b/resources/ci/common/common.py index e8b871a7..7066fa8d 100644 --- a/resources/ci/common/common.py +++ b/resources/ci/common/common.py @@ -23,7 +23,7 @@ def prepare_env(): # get app version APP_VERSION = "" - APP_VERSION_FILE = "./resources/app/meta/manifests/app_version.txt" + 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() diff --git a/resources/ci/common/get_upx.py b/resources/ci/common/get_upx.py index 8d71098e..73c6b6f3 100644 --- a/resources/ci/common/get_upx.py +++ b/resources/ci/common/get_upx.py @@ -6,7 +6,7 @@ from shutil import unpack_archive # only do stuff if we don't have a UPX folder -if not os.path.isdir("./upx"): +if not os.path.isdir(os.path.join(".","upx")): # get env vars env = common.prepare_env() # set up download url @@ -25,7 +25,7 @@ if not os.path.isdir("./upx"): print("Getting UPX: " + UPX_FILE) - with open("./" + UPX_FILE,"wb") as upx: + with open(os.path.join(".",UPX_FILE),"wb") as upx: UPX_REQ = urllib.request.Request( UPX_URL, data=None @@ -34,9 +34,9 @@ if not os.path.isdir("./upx"): UPX_DATA = UPX_REQ.read() upx.write(UPX_DATA) - unpack_archive(UPX_FILE,"./") + unpack_archive(UPX_FILE,os.path.join(".")) - os.rename("./" + UPX_SLUG,"./upx") - os.remove("./" + UPX_FILE) + 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("./upx") else "") + "be available.") +print("UPX should " + ("not " if not os.path.isdir(os.path.join(".","upx")) else "") + "be available.") diff --git a/resources/ci/common/git_clean.py b/resources/ci/common/git_clean.py index ecef5507..b6e9668b 100644 --- a/resources/ci/common/git_clean.py +++ b/resources/ci/common/git_clean.py @@ -1,20 +1,28 @@ import subprocess # do stuff at the shell level +import os -def git_clean(): +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 + "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 - "-dfx", # d: directories, f: files, x: ignored files + "-" + switches, *excludes]) if __name__ == "__main__": diff --git a/resources/ci/common/git_cleanest.py b/resources/ci/common/git_cleanest.py new file mode 100644 index 00000000..30f64d53 --- /dev/null +++ b/resources/ci/common/git_cleanest.py @@ -0,0 +1,3 @@ +from git_clean import git_clean + +git_clean(clean_user=True) diff --git a/source/classes/SpriteSelector.py b/source/classes/SpriteSelector.py index 0defb5d6..1ab073ad 100644 --- a/source/classes/SpriteSelector.py +++ b/source/classes/SpriteSelector.py @@ -32,12 +32,13 @@ class SpriteSelector(object): webbrowser.open("http://alttpr.com/sprite_preview") 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 SpriteSomething directory for Link sprites def open_spritesomething_listing(_evt): - webbrowser.open("https://artheau.github.io/SpriteSomething/?mode=zelda3/link") -# webbrowser.open("https://artheau.github.io/SpriteSomething/resources/app/snes/zelda3/link/sprites.html") + webbrowser.open("https://artheau.github.io/SpriteSomething/resources/app/snes/zelda3/link/sprites.html") official_frametitle = Frame(self.window) official_title_text = Label(official_frametitle, text="Official Sprites") @@ -57,8 +58,8 @@ class SpriteSelector(object): spritesomething_title_link.pack(side=LEFT) spritesomething_title_link.bind("", 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(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(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, 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.pack(side=BOTTOM, fill=X, pady=5) @@ -150,10 +151,10 @@ class SpriteSelector(object): try: 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] 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. official_filenames = [filename for (_, filename) in official_sprites] @@ -230,23 +231,23 @@ class SpriteSelector(object): @property def official_sprite_dir(self): - if is_bundled(): - return output_path("sprites/official") +# if is_bundled(): +# return output_path(os.path.join("sprites","official")) return self.local_official_sprite_dir @property def local_official_sprite_dir(self): - return local_path("data/sprites/official") + return local_path(os.path.join("data","sprites","official")) @property def unofficial_sprite_dir(self): - if is_bundled(): - return output_path("sprites/unofficial") +# if is_bundled(): +# return output_path(os.path.join("sprites","unofficial")) return self.local_unofficial_sprite_dir @property 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): diff --git a/source/gui/bottom.py b/source/gui/bottom.py index 218f7785..23ec330f 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -115,6 +115,7 @@ def bottom_frame(self, parent, args=None): 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]+)(.*)" @@ -123,6 +124,7 @@ def bottom_frame(self, parent, args=None): 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))) From fe6383bc5761b2b9d9638b53c6f821a44a7f3353 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Sat, 28 Mar 2020 19:09:10 -0700 Subject: [PATCH 08/11] Fix Spec files --- DungeonRandomizer.spec | 3 +-- Gui.spec | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/DungeonRandomizer.spec b/DungeonRandomizer.spec index 1ef92104..d7234682 100644 --- a/DungeonRandomizer.spec +++ b/DungeonRandomizer.spec @@ -30,8 +30,7 @@ binaries = [] #if sys.platform.find("windows"): # binaries.append(("ucrtbase.dll",".")) -a = Analysis(['DungeonRandomizer.py'], -a = Analysis([f"../{BINARY_SLUG}.py"], +a = Analysis([f"./{BINARY_SLUG}.py"], pathex=[], binaries=binaries, datas=[], diff --git a/Gui.spec b/Gui.spec index 0ccabada..001e82b2 100644 --- a/Gui.spec +++ b/Gui.spec @@ -32,8 +32,7 @@ binaries = [] #if sys.platform.find("windows"): # binaries.append(("ucrtbase.dll",".")) -a = Analysis(['DungeonRandomizer.py'], -a = Analysis([f"../{BINARY_SLUG}.py"], +a = Analysis([f"./{BINARY_SLUG}.py"], pathex=[], binaries=binaries, datas=[], From ab5f2ab521e6e04587d1691146e25f791fa39e1a Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Sun, 5 Apr 2020 09:50:01 -0700 Subject: [PATCH 09/11] JSON Spoiler output --- .gitignore | 1 + GuiUtils.py | 1 + Main.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ed3e8193..1fa1e41c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea .vscode *_Spoiler.txt +*_Spoiler.json *.pyc *.sfc *.srm diff --git a/GuiUtils.py b/GuiUtils.py index f0aee3b9..840e8520 100644 --- a/GuiUtils.py +++ b/GuiUtils.py @@ -1,4 +1,5 @@ import queue +import os import threading import tkinter as tk diff --git a/Main.py b/Main.py index 0cac21e5..19629109 100644 --- a/Main.py +++ b/Main.py @@ -304,6 +304,8 @@ def main(args, seed=None, fish=None): elif args.create_spoiler: logger.info(world.fish.translate("cli","cli","patching.spoiler")) world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase)) + with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: + outfile.write(world.spoiler.to_json()) YES = world.fish.translate("cli","cli","yes") NO = world.fish.translate("cli","cli","no") From c9d6b87d94700a87cfa8de13d8b4cbe6fa63aee8 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Sun, 5 Apr 2020 12:14:41 -0700 Subject: [PATCH 10/11] Update .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 1fa1e41c..a78ae461 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .idea .vscode *_Spoiler.txt +*.bmbp +*.log *_Spoiler.json *.pyc *.sfc @@ -19,6 +21,9 @@ EnemizerCLI/ RaceRom.py upx/ weights/ +/MultiMystery/ +/Players/ +/QUsb2Snes/ resources/user/* !resources/user/.gitkeep From 4c35b39e35284fb9afee5fe932f08c31c5fd3638 Mon Sep 17 00:00:00 2001 From: "Mike A. Trethewey" Date: Tue, 7 Apr 2020 02:17:25 -0700 Subject: [PATCH 11/11] Fix Hints toggle on CLI --- CLI.py | 11 +++-- Utils.py | 97 ++++++++++++++++++++++++------------- resources/app/cli/args.json | 11 +++-- 3 files changed, 78 insertions(+), 41 deletions(-) diff --git a/CLI.py b/CLI.py index dabc7ed1..b953e322 100644 --- a/CLI.py +++ b/CLI.py @@ -123,6 +123,7 @@ def parse_settings(): "accessibility": "items", "algorithm": "balanced", + # Shuffle Ganon defaults to TRUE "openpyramid": False, "shuffleganon": True, "shuffle": "vanilla", @@ -146,7 +147,9 @@ def parse_settings(): "multi": 1, "names": "", + # Hints default to TRUE "hints": True, + "no_hints": False, "disablemusic": False, "quickswap": False, "heartcolor": "red", @@ -156,11 +159,11 @@ def parse_settings(): "ow_palettes": "default", "uw_palettes": "default", - "suppress_spoiler": True, + # Spoiler defaults to FALSE + # Playthrough defaults to TRUE + # ROM defaults to TRUE "create_spoiler": False, - "skip_playthrough": False, "calc_playthrough": True, - "suppress_rom": False, "create_rom": True, "usestartinventory": False, "custom": False, @@ -310,9 +313,9 @@ def get_args_priority(settings_args, gui_args, cli_args): else: newArgs[key] = args[key] - newArgs[key] = update_deprecated_args(newArgs[key]) else: newArgs[key] = args[key] + newArgs[key] = update_deprecated_args(newArgs[key]) args = newArgs diff --git a/Utils.py b/Utils.py index 1735dfe6..964558a8 100644 --- a/Utils.py +++ b/Utils.py @@ -249,43 +249,72 @@ def print_wiki_doors_by_region(d_regions, world, player): f.write(toprint) def update_deprecated_args(args): - argVars = vars(args) - truthy = [ 1, True, "True", "true" ] - # Don't do: Yes - # Do: No - if "suppress_spoiler" in argVars: - args.create_spoiler = args.suppress_spoiler not in truthy - # Don't do: No - # Do: Yes - if "create_spoiler" in argVars: - args.suppress_spoiler = not args.create_spoiler in truthy + 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] = not argVars[src] in truthy # tmp = !src + args.hints = tmp # dest = tmp + else: + args.hints = not args.no_hints 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] = not argVars[src] in truthy # tmp = !src + args.no_hints = tmp # dest = tmp + else: + args.no_hints = not args.hints in truthy # dest = !src - # Don't do: Yes - # Do: No - if "suppress_rom" in argVars: - args.create_rom = args.suppress_rom not in truthy - # Don't do: No - # Do: Yes - if "create_rom" in argVars: - args.suppress_rom = not args.create_rom in truthy + # 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 - # 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 + # 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 - # 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 + # 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 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index e74e1736..11d14887 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -5,11 +5,11 @@ "type": "bool" }, "create_spoiler": { - "action": "store_false", + "action": "store_true", "type": "bool" }, "suppress_spoiler": { - "action": "store_true", + "action": "store_false", "dest": "create_spoiler", "help": "suppress" }, @@ -204,9 +204,14 @@ ] }, "hints": { - "action": "store_true", + "action": "store_false", "type": "bool" }, + "no_hints": { + "action": "store_true", + "dest": "hints", + "help": "suppress" + }, "heartbeep": { "choices": [ "normal",