From 18b33f68226ac29e70a194c8d2cfef40575e31b1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 21 May 2024 08:26:16 -0600 Subject: [PATCH 01/28] fix: typo --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index a31fdddc..5f2adfb9 100644 --- a/Rom.py +++ b/Rom.py @@ -1268,7 +1268,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): "Skull Woods": 0x0080, "Swamp Palace": 0x0400, "Ice Palace": 0x0040, - "Misery Mire'": 0x0100, + "Misery Mire": 0x0100, "Turtle Rock": 0x0008, } From a5023a18a6d3445199f0cbe71ca02e6d47645c26 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 23 May 2024 09:58:52 -0600 Subject: [PATCH 02/28] change: default port for MultiClient snes connection is now 23074 --- MultiClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MultiClient.py b/MultiClient.py index a4c3b95f..d8d3b663 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -1069,7 +1069,7 @@ async def game_watcher(ctx : Context): async def main(): parser = argparse.ArgumentParser() - parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.') + parser.add_argument('--snes', default='localhost:23074', help='Address of the QUsb2snes/SNI server.') parser.add_argument('--connect', default=None, help='Address of the multiworld host.') parser.add_argument('--password', default=None, help='Password of the multiworld host.') parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) From 28872c8d273f8e76b08fb74407d31e603e7b7f33 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 28 May 2024 14:52:24 -0600 Subject: [PATCH 03/28] feat: skull woods er options --- BaseClasses.py | 14 + CLI.py | 3 + ItemList.py | 23 +- Main.py | 4 +- RELEASENOTES.md | 46 +++ mystery_testsuite.yml | 9 + resources/app/cli/args.json | 15 + resources/app/cli/lang/en.json | 14 + resources/app/gui/lang/en.json | 11 + .../app/gui/randomize/entrando/widgets.json | 20 ++ source/classes/CustomSettings.py | 4 + source/classes/constants.py | 2 + source/overworld/EntranceShuffle2.py | 327 +++++++++++++----- source/tools/MysteryUtils.py | 2 + 14 files changed, 411 insertions(+), 83 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9cb3e2a4..868eac4e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2551,6 +2551,8 @@ class Spoiler(object): 'shuffleganon': self.world.shuffle_ganon, 'shufflelinks': self.world.shufflelinks, 'shuffletavern': self.world.shuffletavern, + 'skullwoods': self.world.skullwoods, + 'linked_drops': self.world.linked_drops, 'take_any': self.world.take_any, 'overworld_map': self.world.overworld_map, 'door_shuffle': self.world.doorShuffle, @@ -2779,6 +2781,9 @@ class Spoiler(object): if self.metadata['shuffle'][player] != 'vanilla': outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") + outfile.write(f"Skull Woods Shuffle: {self.metadata['skullwoods'][player]}\n") + if self.metadata['linked_drops'] != "unset": + outfile.write(f"Linked Drops Override: {self.metadata['linked_drops'][player]}\n") outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) @@ -3120,6 +3125,10 @@ overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} +# byte 13: SSDD ???? (skullwoods, linked_drops, 4 free bytes) +skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} +linked_drops_mode = {'unset': 0, 'linked': 1, 'independent': 2} + # sfx_shuffle and other adjust items does not affect settings code # Bump this when making changes that are not backwards compatible (nearly all of them) @@ -3169,6 +3178,8 @@ class Settings(object): ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), + + (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3242,6 +3253,9 @@ class Settings(object): args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 5] args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x18) >> 3] args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07] + if len(settings) > 13: + args.skullwoods[p] = r(skullwoods_mode)[(settings[13] & 0xc0) >> 6] + args.linked_drops[p] = r(linked_drops_mode)[(settings[13] & 0x30) >> 4] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 18a1d72b..cae0feb8 100644 --- a/CLI.py +++ b/CLI.py @@ -135,6 +135,7 @@ def parse_cli(argv, no_defaults=False): 'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items', 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', + 'skullwoods', 'linked_drops', 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', @@ -191,6 +192,8 @@ def parse_settings(): "shuffle": "vanilla", "shufflelinks": False, "shuffletavern": True, + 'skullwoods': 'original', + 'linked_drops': 'unset', "overworld_map": "default", 'take_any': 'none', "pseudoboots": False, diff --git a/ItemList.py b/ItemList.py index bba6edec..6764db40 100644 --- a/ItemList.py +++ b/ItemList.py @@ -10,7 +10,7 @@ from PotShuffle import vanilla_pots from Items import ItemFactory from source.dungeon.EnemyList import add_drop_contents -from source.overworld.EntranceShuffle2 import connect_entrance +from source.overworld.EntranceShuffle2 import exit_ids, door_addresses from source.item.FillUtil import trash_items, pot_items import source.classes.constants as CONST @@ -549,6 +549,27 @@ def set_up_take_anys(world, player, skip_adjustments=False): world.initialize_regions() +def connect_entrance(world, entrancename, exitname, player): + entrance = world.get_entrance(entrancename, player) + # check if we got an entrance or a region to connect to + try: + region = world.get_region(exitname, player) + exit = None + except RuntimeError: + exit = world.get_entrance(exitname, player) + region = exit.parent_region + + # if this was already connected somewhere, remove the backreference + if entrance.connected_region is not None: + entrance.connected_region.entrances.remove(entrance) + + target = exit_ids[exit.name][0] if exit is not None else exit_ids.get(region.name, None) + addresses = door_addresses[entrance.name][0] + + entrance.connect(region, addresses, target) + world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player) + + def create_dynamic_shop_locations(world, player): for shop in world.shops[player]: if shop.region.player == player: diff --git a/Main.py b/Main.py index 983d4673..43b0b127 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.1.12' +version_number = '1.4.2' version_branch = '-u' __version__ = f'{version_number}{version_branch}' @@ -135,6 +135,8 @@ def main(args, seed=None, fish=None): world.standardize_palettes = args.standardize_palettes.copy() world.shufflelinks = args.shufflelinks.copy() world.shuffletavern = args.shuffletavern.copy() + world.skullwoods = args.skullwoods.copy() + world.linked_drops = args.linked_drops.copy() world.pseudoboots = args.pseudoboots.copy() world.overworld_map = args.overworld_map.copy() world.take_any = args.take_any.copy() diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e1a5a605..99b59d32 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -125,6 +125,47 @@ Designed by Codemann, these are available now (only with experimental turned on - Both dungeons and connectors can be cross-world connections - No dungeon guarantees like in Lite ER +### Skull Woods Shuffle + +In an effort to reduce annoying Skull Woods layouts, several new options have been created. + +- Original: Skull woods shuffles classically amongst itself unless insanity is the mode. This should mimic prior behavior. +- Restricted (Vanilla Drops, Entrances Restricted): Skull woods drops are vanilla. Skull woods entrances stay in skull woods and are shuffled. +- Loose (Vanilla Drops, Entrances use Shuffle): Skull woods drops are vanilla. The main ER mode's pool determines how to handle. +- Followdrops (Follow Linked Drops Setting): This looks at the new linked drop settings. If linked drops are turned on, then two new pairs of linked drop down and holes are formed. Skull front and the hole near the big chest form a pair. The east entrance to Skull 2 and th hole in the back of skull woods form another pair. If the mode is not a cross-world shuffle, then these 2 new drop-down pairs are limited to the dark world. The other drop-down in skull woods, the front two holes will be vanilla. If linked drops are off, then the mode determines how to handle the holes and entrances. + +### Linked Drops Override + +This controls whether drops should be linked to nearby entrances or not. + +- Unset: This uses the mode's default which is considered linked for all modes except insanity +- Linked: Forces drops to be linked to their entrances. +- Independent: Decouples drops from their entrances. In same-world shuffles, holes & entrances may be restricted to a singe world depending on settings and placement to prevent cross-world connection through holes and/or entrances in dungeons. + +### Brief Explanations + +Loose Skull Woods Shuffle: + +- Simple dungeons modes will attempt to fix the layout of skull woods to be more vanilla. This includes dungeonssimple, simple, and restricted ER modes. +- The dungeonsfull mode allows skull woods to be used as a connector but attempt to maintain same-world connectivity. +- Same world modes like lite & full will generally keep skull woods entrances to a single world to prevent cross-world connections. If not inverted, this is not guaranteed to be the dark world though. +- Cross-world modes will be eaiser to comprehend due to fewer restrictions like crossed, lean and swapped. + +Followdrops with Linked Drops: + +- Some modes don't care much about linked drops: simple, dungeonssimple, dungeonsfull +- Same-world modes like restricted, full, & lite will often keep skull woods drop pairs in the dark world and there are only 3 options there: pyramid, and the vanilla locations +- Cross-world modes will benefit the most from the changes as the drop pool expands by two new options for drop placement and guarantees a way out from skull woods west, though the connector must be located. +- Insanity with linked drops will kind of allow a player to scout holes, at the cost of not being to get back to the hole immediately. + +Followdrops with Independent Drops: +- dungeonssimple will place holes vanilla anyway +- dungeonsfull will shuffle the holes +- Same-world modes like simple, restricted, full, & lite will likely pull all skull woods entrances to a single world. (It'll likely be the light world if a single hole is in the light world, unless inverted, then the reverse.) +- Cross-world modes like swapped, lean, and crossed will mean drops are no longer scoutable. Enjoy your coin flips! +- This is insanity's default anyway, no change. + + ### Back of Tavern Shuffle (Experimental required) Thanks goes to Catobat which now allows the back of tavern to be shuffled anywhere and any valid cave can be at the back of tavern with this option checked. Available in experimental only for now as it requires the new algorithm to be shuffled properly. @@ -141,6 +182,11 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes +* 1.4.2 + * New ER Options: + * [Skull Woods shuffle options](#skull-woods-shuffle) + * [New option](#linked-drops-override) to override linked drop down behavior + * MultiClient: change default port to 23074 for newer SNI versions * 1.4.1.12u * New Entrance Shuffle Algorithm no longer experimental * Back of Tavern Shuffle now on by default diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index d3a83234..8d54f848 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -71,6 +71,15 @@ shufflelinks: shuffletavern: on: 1 off: 1 +skullwoods: + original: 1 + restricted: 1 + loose: 1 + followlinked: 1 +linked_drops: + unset: 1 + linked: 1 + independent: 1 world_state: standard: 1 open: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index c2d31606..415ab50f 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -439,6 +439,21 @@ "action": "store_true", "type": "bool" }, + "skullwoods": { + "choices": [ + "original", + "restricted", + "loose", + "followlinked" + ] + }, + "linked_drops": { + "choices": [ + "unset", + "linked", + "independent" + ] + }, "overworld_map": { "choices": [ "default", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index b57508fe..47501d95 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -385,6 +385,20 @@ "shuffletavern": [ "Include the back of the tavern in the entrance shuffle pool. (default: %(default)s)" ], + "skullwoods": [ + "Select how to shuffle skull woods (default: %(default)s)", + "Original: Skull Woods is shuffled amongst itself", + "Restricted: Drops are vanilla. Entrances stay in skull woods", + "Loose: Drops are vanilla. Entrances go in the main pool", + "Followlinked: If drops are linked, then pinball/left side will be vanilla", + " with other drops paired with an entrance. Otherwise, all go into the main pool" + ], + "linked_drops": [ + "Select how drops are treated in entrance shuffle. (default: %(default)s)", + "Unset: The shuffle mode determines the setting.", + "Linked: Dropdowns will be linked with entrance caves", + "Independent: Dropdowns will not be linked" + ], "overworld_map": [ "Control if and how the overworld map indicators show the locations of dungeons (default: %(default)s)" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index aa8be106..a0a5677b 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -162,6 +162,17 @@ "randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full", "randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple", + "randomizer.entrance.skullwoods": "Skull Woods Shuffle", + "randomizer.entrance.skullwoods.original": "Original", + "randomizer.entrance.skullwoods.restricted": "Vanilla Drops, Entrances Restricted", + "randomizer.entrance.skullwoods.loose": "Vanilla Drops, Entrances use Shuffle", + "randomizer.entrance.skullwoods.followlinked": "Follow Linked Drops Setting", + + "randomizer.entrance.linked_drops": "Linked Drops Override", + "randomizer.entrance.linked_drops.unset": "Determined by Shuffle", + "randomizer.entrance.linked_drops.linked": "Always Linked", + "randomizer.entrance.linked_drops.independent": "Independent", + "randomizer.gameoptions.nobgm": "Disable Music & MSU-1", "randomizer.gameoptions.quickswap": "L/R Quickswapping", "randomizer.gameoptions.reduce_flashing": "Reduce Flashing", diff --git a/resources/app/gui/randomize/entrando/widgets.json b/resources/app/gui/randomize/entrando/widgets.json index c325ea26..164b3f4c 100644 --- a/resources/app/gui/randomize/entrando/widgets.json +++ b/resources/app/gui/randomize/entrando/widgets.json @@ -34,6 +34,26 @@ "padx": [20,0] } }, + "skullwoods": { + "type": "selectbox", + "options": [ + "original", + "restricted", + "loose", + "followlinked" + ], + "config": { + "width": 30 + } + }, + "linked_drops": { + "type": "selectbox", + "options": [ + "unset", + "linked", + "independent" + ] + }, "openpyramid": { "type": "selectbox", "options": [ diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 14084f0b..51fa4c2e 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -143,6 +143,8 @@ class CustomSettings(object): args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p]) args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p]) args.shuffletavern[p] = get_setting(settings['shuffletavern'], args.shuffletavern[p]) + args.skullwoods[p] = get_setting(settings['skullwoods'], args.skullwoods[p]) + args.linked_drops[p] = get_setting(settings['linked_drops'], args.linked_drops[p]) args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p]) args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p]) args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p]) @@ -279,6 +281,8 @@ class CustomSettings(object): settings_dict[p]['bombbag'] = world.bombbag[p] settings_dict[p]['shufflelinks'] = world.shufflelinks[p] settings_dict[p]['shuffletavern'] = world.shuffletavern[p] + settings_dict[p]['skullwoods'] = world.skullwoods[p] + settings_dict[p]['linked_drops'] = world.linked_drops[p] settings_dict[p]['overworld_map'] = world.overworld_map[p] settings_dict[p]['pseudoboots'] = world.pseudoboots[p] settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 0950211e..7c213ff9 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -92,6 +92,8 @@ SETTINGSTOPROCESS = { "shuffleganon": "shuffleganon", "shufflelinks": "shufflelinks", "shuffletavern": "shuffletavern", + "skullwoods": "skullwoods", + "linked_drops": "linked_drops", "openpyramid": "openpyramid", "overworld_map": "overworld_map", }, diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index cad11db6..9dfb7de9 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -23,6 +23,7 @@ class EntrancePool(object): self.decoupled_exits = [] self.original_entrances = set() self.original_exits = set() + self.same_world_restricted = {} self.world = world self.player = player @@ -90,6 +91,10 @@ def link_entrances_new(world, player): if mode not in modes: raise RuntimeError(f'Shuffle mode {mode} is not yet supported') mode_cfg = copy.deepcopy(modes[mode]) + + if world.linked_drops[player] != 'unset': + mode_cfg['keep_drops_together'] = 'on' if world.linked_drops[player] == 'linked' else 'off' + avail_pool.swapped = mode_cfg['undefined'] == 'swap' if avail_pool.is_standard(): do_standard_connections(avail_pool) @@ -97,11 +102,7 @@ def link_entrances_new(world, player): for pool_name, pool in pool_list.items(): special_shuffle = pool['special'] if 'special' in pool else None if special_shuffle == 'drops': - holes, targets = find_entrances_and_targets_drops(avail_pool, pool['entrances']) - if avail_pool.swapped: - connect_swapped(holes, targets, avail_pool) - else: - connect_random(holes, targets, avail_pool) + handle_skull_woods_drops(avail_pool, pool['entrances'], mode_cfg) elif special_shuffle == 'fixed_shuffle': do_fixed_shuffle(avail_pool, pool['entrances']) elif special_shuffle == 'same_world': @@ -124,12 +125,7 @@ def link_entrances_new(world, player): elif special_shuffle == 'vanilla': do_vanilla_connect(pool, avail_pool) elif special_shuffle == 'skull': - entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) - if avail_pool.swapped: - connect_swapped(entrances, exits, avail_pool, True) - else: - connect_random(entrances, exits, avail_pool, True) - avail_pool.skull_handled = True + handle_skull_woods_entrances(avail_pool, pool['entrances']) else: entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) do_main_shuffle(entrances, exits, avail_pool, mode_cfg) @@ -239,12 +235,13 @@ def do_main_shuffle(entrances, exits, avail, mode_def): # mandatory exits rem_entrances, rem_exits = set(), set() if not cross_world: + determine_dungeon_restrictions(avail) mand_exits = figure_out_must_exits_same_world(entrances, exits, avail) - must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves, hyrule_forced = mand_exits - if hyrule_forced: - do_mandatory_connections(avail, lw_entrances, hyrule_forced, must_exit_lw) - else: - do_mandatory_connections(avail, lw_entrances, multi_exit_caves, must_exit_lw) + must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves = mand_exits + lw_candidates = filter_restricted_caves(multi_exit_caves, 'LightWorld', avail) + other_candidates = [x for x in multi_exit_caves if x not in lw_candidates] # remember those not passed in + do_mandatory_connections(avail, lw_entrances, lw_candidates, must_exit_lw) + multi_exit_caves = other_candidates + lw_candidates # rebuild list from the lw_candidates and those not passed # remove old man house as connector - not valid for dw must_exit if it is a spawn point if not avail.inverted: new_mec = [] @@ -254,7 +251,10 @@ def do_main_shuffle(entrances, exits, avail, mode_def): else: new_mec.append(cave_option) multi_exit_caves = new_mec - do_mandatory_connections(avail, dw_entrances, multi_exit_caves, must_exit_dw) + dw_candidates = filter_restricted_caves(multi_exit_caves, 'DarkWorld', avail) + other_candidates = [x for x in multi_exit_caves if x not in dw_candidates] # remember those not passed in + do_mandatory_connections(avail, dw_entrances, dw_candidates, must_exit_dw) + multi_exit_caves = other_candidates + dw_candidates # rebuild list from the dw_candidates and those not passed rem_entrances.update(lw_entrances) rem_entrances.update(dw_entrances) else: @@ -351,6 +351,10 @@ def do_main_shuffle(entrances, exits, avail, mode_def): if bonk_fairy_exception(x): lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) + if avail.world.doorShuffle[avail.player] != 'vanilla': + determine_dungeon_restrictions(avail) + possibles = figure_out_possible_exits(rem_exits) + do_same_world_possible_connectors(lw_entrances, dw_entrances, possibles, avail) unused_entrances.update(lw_entrances) unused_entrances.update(dw_entrances) else: @@ -436,12 +440,16 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe if not keep_together: targets = [avail.one_way_map[x] for x in holes_to_shuffle] - connect_random(holes_to_shuffle, targets, avail) + if avail.swapped: + connect_swapped(holes_to_shuffle, targets, avail) + else: + connect_random(holes_to_shuffle, targets, avail) remove_from_list(entrances, holes_to_shuffle) remove_from_list(exits, targets) return # we're done here hole_entrances, hole_targets = [], [] + leftover_hole_entrances, leftover_hole_targets = [], [] for hole in drop_map: if hole in avail.original_entrances and hole in linked_drop_map: linked_entrance = linked_drop_map[hole] @@ -451,40 +459,39 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe target_drop = avail.one_way_map[hole] if target_exit in exits and target_drop in exits: hole_targets.append((target_exit, target_drop)) + else: + if hole in avail.original_entrances and hole in entrances: + leftover_hole_entrances.append(hole) + if drop_map[hole] in exits: + leftover_hole_targets.append(drop_map[hole]) random.shuffle(hole_entrances) - if not cross_world and 'Sanctuary Grave' in holes_to_shuffle: - hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) - is_hc_in_dw = avail.world.mode[avail.player] == 'inverted' - if hc.connected_region: - is_hc_in_dw = hc.connected_region.type == RegionType.DarkWorld - chosen_entrance = None - if is_hc_in_dw: - if avail.swapped: - chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances and e[0] != 'Sanctuary') + if not cross_world: + if 'Sanctuary Grave' in holes_to_shuffle: + hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) + is_hc_in_dw = avail.world.mode[avail.player] == 'inverted' + if hc.connected_region: + is_hc_in_dw = hc.connected_region.type == RegionType.DarkWorld + chosen_entrance = None + if is_hc_in_dw: + if avail.swapped: + chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances and e[0] != 'Sanctuary') + if not chosen_entrance: + chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) if not chosen_entrance: - chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) - if not chosen_entrance: - if avail.swapped: - chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances and e[0] != 'Sanctuary') - if not chosen_entrance: - chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances) + if avail.swapped: + chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances and e[0] != 'Sanctuary') + if not chosen_entrance: + chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances) - if chosen_entrance: - hole_entrances.remove(chosen_entrance) - sanc_interior = next(target for target in hole_targets if target[0] == 'Sanctuary Exit') - hole_targets.remove(sanc_interior) - connect_two_way(chosen_entrance[0], sanc_interior[0], avail) # two-way exit - connect_entrance(chosen_entrance[1], sanc_interior[1], avail) # hole - remove_from_list(entrances, [chosen_entrance[0], chosen_entrance[1]]) - remove_from_list(exits, [sanc_interior[0], sanc_interior[1]]) - if avail.swapped and drop_map[chosen_entrance[1]] != sanc_interior[1]: - swap_ent, swap_ext = connect_swap(chosen_entrance[0], sanc_interior[0], avail) - swap_drop, swap_tgt = connect_swap(chosen_entrance[1], sanc_interior[1], avail) - hole_entrances.remove((swap_ent, swap_drop)) - hole_targets.remove((swap_ext, swap_tgt)) - remove_from_list(entrances, [swap_ent, swap_drop]) - remove_from_list(exits, [swap_ext, swap_tgt]) + if chosen_entrance: + connect_hole_via_interior(chosen_entrance, 'Sanctuary Exit', hole_entrances, hole_targets, entrances, exits, avail) + if 'Skull Woods First Section Hole (North)' in holes_to_shuffle: + chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) + connect_hole_via_interior(chosen_entrance, 'Skull Woods First Section Exit', hole_entrances, hole_targets, entrances, exits, avail) + if 'Skull Woods Second Section Hole' in holes_to_shuffle: + chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) + connect_hole_via_interior(chosen_entrance, 'Skull Woods Second Section Exit (East)', hole_entrances, hole_targets, entrances, exits, avail) random.shuffle(hole_targets) while len(hole_entrances): @@ -506,6 +513,31 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe remove_from_list(entrances, [swap_ent, swap_drop]) remove_from_list(exits, [swap_ext, swap_tgt]) + if leftover_hole_entrances and leftover_hole_targets: + remove_from_list(entrances, leftover_hole_entrances) + remove_from_list(exits, leftover_hole_targets) + if avail.swapped: + connect_swapped(leftover_hole_entrances, leftover_hole_targets, avail) + else: + connect_random(leftover_hole_entrances, leftover_hole_targets, avail) + + +def connect_hole_via_interior(chosen_entrance, interior, hole_entrances, hole_targets, entrances, exits, avail): + hole_entrances.remove(chosen_entrance) + interior = next(target for target in hole_targets if target[0] == interior) + hole_targets.remove(interior) + connect_two_way(chosen_entrance[0], interior[0], avail) + connect_entrance(chosen_entrance[1], interior[1], avail) + remove_from_list(entrances, [chosen_entrance[0], chosen_entrance[1]]) + remove_from_list(exits, [interior[0], interior[1]]) + if avail.swapped and drop_map[chosen_entrance[1]] != interior[1]: + swap_ent, swap_ext = connect_swap(chosen_entrance[0], interior[0], avail) + swap_drop, swap_tgt = connect_swap(chosen_entrance[1], interior[1], avail) + hole_entrances.remove((swap_ent, swap_drop)) + hole_targets.remove((swap_ext, swap_tgt)) + remove_from_list(entrances, [swap_ent, swap_drop]) + remove_from_list(exits, [swap_ext, swap_tgt]) + def do_links_house(entrances, exits, avail, cross_world): lh_exit = 'Links House Exit' @@ -628,38 +660,77 @@ def figure_out_true_exits(exits, avail): return multi_exit_caves -# todo: figure out hyrule forced better +def figure_out_possible_exits(exits): + possible_multi_exit_caves = [] + for item in doors_possible_connectors: + if item in exits: + remove_from_list(exits, item) + possible_multi_exit_caves.append(item) + return possible_multi_exit_caves + + +def determine_dungeon_restrictions(avail): + check_for_hc = (avail.is_standard() or avail.world.doorShuffle[avail.player] != 'vanilla') + for check in dungeon_restriction_checks: + dungeon_exits, drop_regions = check + if check_for_hc and any('Hyrule Castle' in x for x in dungeon_exits): + avail.same_world_restricted.update({x: 'LightWorld' for x in dungeon_exits}) + else: + restriction = None + for x in dungeon_exits: + ent = avail.world.get_entrance(x, avail.player) + if ent.connected_region: + if ent.connected_region.type == RegionType.LightWorld: + restriction = 'LightWorld' + elif ent.connected_region.type == RegionType.DarkWorld: + restriction = 'DarkWorld' + # Holes only restrict + for x in drop_regions: + region = avail.world.get_region(x, avail.player) + ent = next((ent for ent in region.entrances if ent.parent_region and ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]), None) + if ent: + if ent.parent_region.type == RegionType.LightWorld and not avail.inverted: + restriction = 'LightWorld' + elif ent.parent_region.type == RegionType.DarkWorld and avail.inverted: + restriction = 'DarkWorld' + if restriction: + avail.same_world_restricted.update({x: restriction for x in dungeon_exits}) + + def figure_out_must_exits_same_world(entrances, exits, avail): lw_entrances, dw_entrances = [], [] - hyrule_forced = None - check_for_hc = (avail.is_standard() or avail.world.doorShuffle[avail.player] != 'vanilla') for x in entrances: lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) + multi_exit_caves = figure_out_connectors(exits) - if check_for_hc: - for option in multi_exit_caves: - if any(x in option for x in ['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (East)', - 'Hyrule Castle Exit (West)']): - hyrule_forced = [option] - if hyrule_forced: - remove_from_list(multi_exit_caves, hyrule_forced) + if not avail.inverted and not avail.skull_handled: + skull_connector = [x for x in ['Skull Woods Second Section Exit (West)', 'Skull Woods Second Section Exit (East)'] if x in exits] + multi_exit_caves.append(skull_connector) must_exit_lw, must_exit_dw, unfiltered_lw, unfiltered_dw = must_exits_helper(avail, lw_entrances, dw_entrances) - return must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves, hyrule_forced + return must_exit_lw, must_exit_dw, lw_entrances, dw_entrances, multi_exit_caves def must_exits_helper(avail, lw_entrances, dw_entrances): must_exit_lw_orig = (Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit).copy() must_exit_dw_orig = (Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit).copy() if not avail.inverted and not avail.skull_handled: - must_exit_dw_orig.append(('Skull Woods Second Section Door (West)', 'Skull Woods Final Section')) + must_exit_dw_orig.append('Skull Woods Second Section Door (West)') must_exit_lw = must_exit_filter(avail, must_exit_lw_orig, lw_entrances) must_exit_dw = must_exit_filter(avail, must_exit_dw_orig, dw_entrances) return must_exit_lw, must_exit_dw, flatten(must_exit_lw_orig), flatten(must_exit_dw_orig) +def filter_restricted_caves(multi_exit_caves, restriction, avail): + candidates = [] + for cave in multi_exit_caves: + if all(x not in avail.same_world_restricted or avail.same_world_restricted[x] == restriction for x in cave): + candidates.append(cave) + return candidates + + def flatten(list_to_flatten): ret = [] for item in list_to_flatten: @@ -672,11 +743,14 @@ def flatten(list_to_flatten): def figure_out_must_exits_cross_world(entrances, exits, avail): multi_exit_caves = figure_out_connectors(exits) + if not avail.skull_handled: + skull_connector = [x for x in ['Skull Woods Second Section Exit (West)', 'Skull Woods Second Section Exit (East)'] if x in exits] + multi_exit_caves.append(skull_connector) must_exit_lw = (Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit).copy() must_exit_dw = (Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit).copy() if not avail.inverted and not avail.skull_handled: - must_exit_dw.append(('Skull Woods Second Section Door (West)', 'Skull Woods Final Section')) + must_exit_dw.append('Skull Woods Second Section Door (West)') must_exit = must_exit_filter(avail, must_exit_lw + must_exit_dw, entrances) return must_exit, multi_exit_caves @@ -687,7 +761,7 @@ def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): random.shuffle(dw_entrances) random.shuffle(caves) while caves: - # connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill + # connect highest-exit-count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill cave_candidate = (None, 0) for i, cave in enumerate(caves): if isinstance(cave, str): @@ -696,12 +770,19 @@ def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): cave_candidate = (i, len(cave)) cave = caves.pop(cave_candidate[0]) - target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances if isinstance(cave, str): cave = (cave,) + target, restriction = None, None + if any(x in avail.same_world_restricted for x in cave): + restriction = next(avail.same_world_restricted[x] for x in cave if x in avail.same_world_restricted) + target = lw_entrances if restriction == 'LightWorld' else dw_entrances + if target is None: + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances # check if we can still fit the cave into our target group if len(target) < len(cave): + if restriction: + raise Exception('Not enough entrances for restricted cave, algorithm needs revision (main)') # need to use other set target = lw_entrances if target is dw_entrances else dw_entrances @@ -715,6 +796,18 @@ def do_same_world_connectors(lw_entrances, dw_entrances, caves, avail): connect_two_way(target.pop(), ext, avail) +def do_same_world_possible_connectors(lw_entrances, dw_entrances, possibles, avail): + random.shuffle(possibles) + while possibles: + possible = possibles.pop() + target = None + if possible in avail.same_world_restricted: + target = lw_entrances if avail.same_world_restricted[possible] == 'LightWorld' else dw_entrances + if target is None: + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances + connect_two_way(target.pop(), possible, avail) + determine_dungeon_restrictions(avail) + def do_cross_world_connectors(entrances, caves, avail): random.shuffle(entrances) random.shuffle(caves) @@ -762,6 +855,37 @@ def do_cross_world_connectors(entrances, caves, avail): break +def handle_skull_woods_drops(avail, pool, mode_cfg): + skull_woods = avail.world.skullwoods[avail.player] + if skull_woods in ['restricted', 'loose']: + for drop in pool: + target = drop_map[drop] + connect_entrance(drop, target, avail) + elif skull_woods == 'original': + holes, targets = find_entrances_and_targets_drops(avail, pool) + if avail.swapped: + connect_swapped(holes, targets, avail) + else: + connect_random(holes, targets, avail) + elif skull_woods == 'followlinked': + keep_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True + if keep_together: + for drop in ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)']: + target = drop_map[drop] + connect_entrance(drop, target, avail) + + +def handle_skull_woods_entrances(avail, pool): + skull_woods = avail.world.skullwoods[avail.player] + if skull_woods in ['restricted', 'original']: + entrances, exits = find_entrances_and_exits(avail, pool) + if avail.swapped: + connect_swapped(entrances, exits, avail, True) + else: + connect_random(entrances, exits, avail, True) + avail.skull_handled = True + + def do_fixed_shuffle(avail, entrance_list): max_size = 0 options = {} @@ -803,8 +927,6 @@ def do_same_world_shuffle(avail, pool_def): multi_exit = pool_def['connectors'] # complete_entrance_set = set() lw_entrances, dw_entrances, multi_exits_caves, other_exits = [], [], [], [] - hyrule_forced = None - check_for_hc = avail.is_standard() or avail.world.doorShuffle[avail.player] != 'vanilla' single_entrances, single_exits = find_entrances_and_exits(avail, single_exit) other_exits.extend(single_exits) @@ -814,12 +936,7 @@ def do_same_world_shuffle(avail, pool_def): for option in multi_exit: multi_entrances, multi_exits = find_entrances_and_exits(avail, option) # complete_entrance_set.update(multi_entrances) - if check_for_hc and any(x in multi_entrances for x in ['Hyrule Castle Entrance (South)', - 'Hyrule Castle Entrance (East)', - 'Hyrule Castle Entrance (West)']): - hyrule_forced = [multi_exits] - else: - multi_exits_caves.append(multi_exits) + multi_exits_caves.append(multi_exits) for x in multi_entrances: (dw_entrances, lw_entrances)[x in LW_Entrances].append(x) @@ -828,11 +945,16 @@ def do_same_world_shuffle(avail, pool_def): must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) must_exit_dw = must_exit_filter(avail, must_exit_dw, dw_entrances) - if hyrule_forced: - do_mandatory_connections(avail, lw_entrances, hyrule_forced, must_exit_lw) - else: - do_mandatory_connections(avail, lw_entrances, multi_exits_caves, must_exit_lw) - do_mandatory_connections(avail, dw_entrances, multi_exits_caves, must_exit_dw) + determine_dungeon_restrictions(avail) + lw_candidates = filter_restricted_caves(multi_exits_caves, 'LightWorld', avail) + other_candidates = [x for x in multi_exits_caves if x not in lw_candidates] # remember those not passed in + do_mandatory_connections(avail, lw_entrances, lw_candidates, must_exit_lw) + multi_exits_caves = other_candidates + lw_candidates # rebuild list from the lw_candidates and those not passed + + dw_candidates = filter_restricted_caves(multi_exits_caves, 'DarkWorld', avail) + other_candidates = [x for x in multi_exits_caves if x not in dw_candidates] # remember those not passed in + do_mandatory_connections(avail, dw_entrances, dw_candidates, must_exit_dw) + multi_exits_caves = other_candidates + dw_candidates # rebuild list from the dw_candidates and those not passed # connect caves random.shuffle(lw_entrances) @@ -845,8 +967,15 @@ def do_same_world_shuffle(avail, pool_def): cave_candidate = (i, len(cave)) cave = multi_exits_caves.pop(cave_candidate[0]) - target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances + target, restriction = None, None + if any(x in avail.same_world_restricted for x in cave): + restriction = next(avail.same_world_restricted[x] for x in cave if x in avail.same_world_restricted) + target = lw_entrances if restriction == 'LightWorld' else dw_entrances + if target is None: + target = lw_entrances if random.randint(0, 1) == 0 else dw_entrances if len(target) < len(cave): # swap because we ran out of entrances in that world + if restriction: + raise Exception('Not enough entrances for restricted cave, algorithm needs revision (dungeonsfull)') target = lw_entrances if target is dw_entrances else dw_entrances for ext in cave: @@ -1386,6 +1515,12 @@ modes = { 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] }, + 'skull_layout': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, 'single_entrance_dungeon': { 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] @@ -1419,13 +1554,15 @@ modes = { 'sanc_flag': 'light_world', # always light world flag 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Agahnims Tower', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', - 'Ganons Tower'], + 'Ganons Tower', 'Desert Palace Entrance (North)', 'Dark Death Mountain Ledge (East)'], 'connectors': [['Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (West)'], ['Desert Palace Entrance (South)', 'Desert Palace Entrance (East)', - 'Desert Palace Entrance (West)', 'Desert Palace Entrance (North)'], + 'Desert Palace Entrance (West)'], ['Turtle Rock', 'Turtle Rock Isolated Ledge Entrance', - 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)']] + 'Dark Death Mountain Ledge (West)'], + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', + 'Skull Woods First Section Door']] }, } }, @@ -1611,6 +1748,12 @@ modes = { 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] }, + 'skull_layout': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, 'single_entrance_dungeon': { 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] @@ -1700,6 +1843,12 @@ modes = { 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] }, + 'skull_layout': { + 'special': 'vanilla', + 'condition': '', + 'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', + 'Skull Woods Second Section Door (West)'] + }, 'single_entrance_dungeon': { 'entrances': ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace', 'Ganons Tower'] @@ -1801,7 +1950,10 @@ linked_drop_map = { 'Lumberjack Tree Tree': 'Lumberjack Tree Cave', 'Sanctuary Grave': 'Sanctuary', 'Pyramid Hole': 'Pyramid Entrance', - 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance' + 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance', + + 'Skull Woods First Section Hole (North)': 'Skull Woods First Section Door', + 'Skull Woods Second Section Hole': 'Skull Woods Second Section Door (East)', } entrance_map = { @@ -2066,6 +2218,19 @@ Connector_Exit_Set = { 'Turtle Rock Isolated Ledge Exit', 'Turtle Rock Ledge Exit (West)' } +dungeon_restriction_checks = [ + (['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Sanctuary Exit'], ['Sewer Drop']), + (['Desert Palace Exit (South)', 'Desert Palace Exit (East)', 'Desert Palace Exit (West)', 'Desert Palace Exit (North)'], []), + (['Turtle Rock Exit (Front)', 'Turtle Rock Isolated Ledge Exit', 'Turtle Rock Ledge Exit (West)', 'Turtle Rock Ledge Exit (East)'], []), + (['Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', 'Skull Woods Final Section Exit'], + ['Skull Pinball', 'Skull Left Drop', 'Skull Pot Circle', 'Skull Back Drop']) + ] + +doors_possible_connectors = [ + 'Sanctuary Exit', 'Desert Palace Exit (North)', 'Skull Woods First Section Exit', + 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', 'Skull Woods Final Section Exit' +] + # Entrances that cannot be used to access a must_exit entrance - symmetrical to allow reverse lookups Must_Exit_Invalid_Connections = defaultdict(set, { 'Dark Death Mountain Ledge (East)': {'Dark Death Mountain Ledge (West)', 'Mimic Cave'}, diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index ec3518fb..aa0d9127 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -131,6 +131,8 @@ def roll_settings(weights): ret.shufflelinks = get_choice_bool('shufflelinks') ret.shuffletavern = get_choice_bool('shuffletavern') + ret.skullwoods = get_choice('skullwoods') + ret.linked_drops = get_choice('linked_drops') ret.pseudoboots = get_choice_bool('pseudoboots') ret.shopsanity = get_choice_bool('shopsanity') keydropshuffle = get_choice_bool('keydropshuffle') From 3554177a6182aca79f5a641027feee6688e57e2e Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 30 May 2024 13:02:22 -0600 Subject: [PATCH 04/28] feat: RandomWeapon for starting items feat: Start with "Big Magic" or "Small Magic" for starting magic meter --- InitialSram.py | 9 +++++++-- Main.py | 13 ++++++++++++- RELEASENOTES.md | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/InitialSram.py b/InitialSram.py index b697823e..0bacb744 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -66,6 +66,7 @@ class InitialSram: starting_arrow_cap_upgrades = 30 starting_bombs = 0 starting_arrows = 0 + starting_magic = 0 startingstate = CollectionState(world) @@ -105,10 +106,10 @@ class InitialSram: if startingstate.has('Magic Upgrade (1/4)', player): equip[0x37B] = 2 - equip[0x36E] = 0x80 + starting_magic = 0x80 elif startingstate.has('Magic Upgrade (1/2)', player): equip[0x37B] = 1 - equip[0x36E] = 0x80 + starting_magic = 0x80 for item in world.precollected_items: if item.player != player: @@ -162,6 +163,7 @@ class InitialSram: arrow_caps = {'Arrow Upgrade (+5)': 5, 'Arrow Upgrade (+10)': 10} bombs = {'Single Bomb': 1, 'Bombs (3)': 3, 'Bombs (10)': 10} arrows = {'Single Arrow': 1, 'Arrows (10)': 10} + magic = {'Big Magic': 0x80, 'Small Magic': 0x10} if item.name in set_table: equip[set_table[item.name][0]] = set_table[item.name][1] @@ -198,9 +200,12 @@ class InitialSram: if item.name != 'Piece of Heart' or equip[0x36B] == 0: equip[0x36C] = min(equip[0x36C] + 0x08, 0xA0) equip[0x36D] = min(equip[0x36D] + 0x08, 0xA0) + elif item.name in magic: + starting_magic += magic[item.name] else: raise RuntimeError(f'Unsupported item in starting equipment: {item.name}') + equip[0x36E] = min(starting_magic, 0x80) equip[0x370] = min(starting_bomb_cap_upgrades, 50) equip[0x371] = min(starting_arrow_cap_upgrades, 70) equip[0x343] = min(starting_bombs, equip[0x370]) diff --git a/Main.py b/Main.py index 43b0b127..f59637d0 100644 --- a/Main.py +++ b/Main.py @@ -231,7 +231,18 @@ def main(args, seed=None, fish=None): if inv_list: for inv_item in inv_list: name = inv_item.strip() - name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)' + if inv_item == 'RandomWeapon': + name = random.choice(['Progressive Bow', 'Hammer', 'Progressive Sword', 'Cane of Somaria', 'Cane of Byrna', 'Fire Rod']) + extra = [] + if name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: + extra.append('Big Magic') + if name == 'Progressive Bow': + extra.extend(['Arrows (10)'] * 3) + for e in extra: + item = ItemFactory(e, p) + if item: + world.push_precollected(item) + name = name if name != 'Ocarina' or world.flute_mode[p] != 'active' else 'Ocarina (Activated)' item = ItemFactory(name, p) if item: world.push_precollected(item) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 99b59d32..45c6c296 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -186,6 +186,8 @@ These are now independent of retro mode and have three options: None, Random, an * New ER Options: * [Skull Woods shuffle options](#skull-woods-shuffle) * [New option](#linked-drops-override) to override linked drop down behavior + * Customizer: You can now start with a "RandomWeapon" in the start inventory section + * Customizer: You may now start with "Big Magic" or "Small Magic" items * MultiClient: change default port to 23074 for newer SNI versions * 1.4.1.12u * New Entrance Shuffle Algorithm no longer experimental From 3ba1472635f938a8879b17c69fac8309ec001cb9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 30 May 2024 13:12:25 -0600 Subject: [PATCH 05/28] fix: typo causing ER generation failure --- Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rules.py b/Rules.py index 131c2e40..26a7c302 100644 --- a/Rules.py +++ b/Rules.py @@ -1798,7 +1798,7 @@ def set_big_bomb_rules(world, player): # 1. flute then basic routes # 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge # -> (Flute or (M and P and West Dark World access) and BR - add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_flute(player) or (state.can_reach('Village of Outcasts Area', 'Region', player) and state.has_Pearl(player) and state.has_Mirror(player))) and basic_routes(state)) + add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_flute(player) or (state.can_reach('Village of Outcasts', 'Region', player) and state.has_Pearl(player) and state.has_Mirror(player))) and basic_routes(state)) elif bombshop_entrance.name in Mirror_from_SDW_entrances: # 1. flute then basic routes # 2. (has South dark world access) use existing mirror spot, mirror again off ledge From 54858500e55ffe9d580cfcfa26cf8793358b6335 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 30 May 2024 14:02:52 -0600 Subject: [PATCH 06/28] fix: suppress warning for missing items if they are in start inventory --- ItemList.py | 1 + RELEASENOTES.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/ItemList.py b/ItemList.py index 6764db40..11b1b09b 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1331,6 +1331,7 @@ def make_customizer_pool(world, player): bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None) if not bow_found: missing_items.append('Progressive Bow') + missing_items = [i for i in missing_items if all(i != start.name or player != start.player for start in world.precollected_items)] if missing_items: logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}') diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 45c6c296..8593d406 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -188,7 +188,9 @@ These are now independent of retro mode and have three options: None, Random, an * [New option](#linked-drops-override) to override linked drop down behavior * Customizer: You can now start with a "RandomWeapon" in the start inventory section * Customizer: You may now start with "Big Magic" or "Small Magic" items + * Customizer: Suppress warning for missing items if they are in start inventory * MultiClient: change default port to 23074 for newer SNI versions + * Generation: Fixed typo causing ER gen failure with Bomb Shop at Graveyard Ledge * 1.4.1.12u * New Entrance Shuffle Algorithm no longer experimental * Back of Tavern Shuffle now on by default From ca40f87daa3dbee51b05f327153bb4261238c47a Mon Sep 17 00:00:00 2001 From: "Minnie A. Trethewey (Mike)" Date: Wed, 15 Feb 2023 22:44:38 -0800 Subject: [PATCH 07/28] CI Changes --- .github/actions/appversion-prepare/action.yml | 51 ++ .github/actions/build/action.yml | 88 ++++ .github/actions/get-parent-dir/action.yml | 41 ++ .github/actions/install/action.yml | 49 ++ .github/actions/release-prepare/action.yml | 77 +++ .github/actions/tag-repo/action.yml | 76 +++ .github/actions/test/action.yml | 97 ++++ .github/workflows/ci.yml | 317 ------------- .github/workflows/release-complete.yml | 47 ++ .github/workflows/release-create.yml | 418 +++++++++++++++++ .gitignore | 6 +- DungeonRandomizer.py | 2 +- Text.py | 8 +- mystery_testsuite.yml | 4 +- resources/app/meta/manifests/app_version.txt | 0 resources/app/meta/manifests/binaries.json | 7 + .../app/meta/manifests/excluded_dlls.json | 34 ++ .../app/meta/manifests/pip_requirements.txt | 7 +- resources/ci/common/common.py | 51 +- resources/ci/common/get_get_pip.py | 6 +- resources/ci/common/get_pipline.py | 440 ++++++++++++++++++ resources/ci/common/get_upx.py | 5 +- resources/ci/common/list_actions.py | 168 +++++++ resources/ci/common/prepare_appversion.py | 12 +- resources/ci/common/prepare_binary.py | 48 +- resources/ci/common/prepare_release.py | 14 +- source/DungeonRandomizer.spec | 68 --- source/Gui.spec | 69 --- source/Template.spec | 98 ++++ source/classes/appversion.py | 17 + source/classes/diags.py | 23 +- source/gui/randomize/generation.py | 2 +- source/meta/build-dr.py | 27 -- source/meta/build-gui.py | 27 -- source/meta/build.py | 155 ++++++ source/meta/run_diags.py | 10 + test/MysteryTestSuite.py | 46 +- test/NewTestSuite.py | 36 +- 38 files changed, 2062 insertions(+), 589 deletions(-) create mode 100644 .github/actions/appversion-prepare/action.yml create mode 100644 .github/actions/build/action.yml create mode 100644 .github/actions/get-parent-dir/action.yml create mode 100644 .github/actions/install/action.yml create mode 100644 .github/actions/release-prepare/action.yml create mode 100644 .github/actions/tag-repo/action.yml create mode 100644 .github/actions/test/action.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release-complete.yml create mode 100644 .github/workflows/release-create.yml create mode 100644 resources/app/meta/manifests/app_version.txt create mode 100644 resources/app/meta/manifests/binaries.json create mode 100644 resources/app/meta/manifests/excluded_dlls.json create mode 100644 resources/ci/common/get_pipline.py create mode 100644 resources/ci/common/list_actions.py delete mode 100644 source/DungeonRandomizer.spec delete mode 100644 source/Gui.spec create mode 100644 source/Template.spec create mode 100644 source/classes/appversion.py delete mode 100644 source/meta/build-dr.py delete mode 100644 source/meta/build-gui.py create mode 100644 source/meta/build.py create mode 100644 source/meta/run_diags.py diff --git a/.github/actions/appversion-prepare/action.yml b/.github/actions/appversion-prepare/action.yml new file mode 100644 index 00000000..a8abdfde --- /dev/null +++ b/.github/actions/appversion-prepare/action.yml @@ -0,0 +1,51 @@ +name: Prepare AppVersion +description: Prepare AppVersion document for later use + +runs: + using: "composite" + steps: + # checkout commit + - name: Checkout commit + shell: bash + run: | + echo "Checkout commit" + - name: Checkout commit + uses: actions/checkout@v4.1.4 + + # Set Run Number + - name: Set Run Number + shell: bash + run: | + echo "Set Run Number" + - name: Set Run Number + id: set_run_number + shell: bash + run: | + GITHUB_RUN_NUMBER="${{ github.run_number }}a${{ github.run_attempt }}" + echo "github_run_number=$GITHUB_RUN_NUMBER" >> $GITHUB_OUTPUT + + # Prepare AppVersion + #TODO: source/classes/appversion.py writes the tag format + - name: 💬Prepare AppVersion + shell: bash + run: | + echo "💬Prepare AppVersion" + - name: Prepare AppVersion + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + GITHUB_RUN_NUMBER: ${{ steps.set_run_number.outputs.github_run_number }} + run: | + python -m source.classes.appversion + python ./resources/ci/common/prepare_appversion.py + + # upload appversion artifact for later step + - name: 🔼Upload AppVersion Artifact + shell: bash + run: | + echo "🔼Upload AppVersion Artifact" + - name: 🔼Upload AppVersion Artifact + uses: actions/upload-artifact@v4.3.3 + with: + name: appversion + path: ./resources/app/meta/manifests/app_version.txt diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 00000000..728e0c12 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,88 @@ +name: Build +description: Build app +inputs: + calling-job: + required: true + description: Job that's calling this one + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +runs: + using: "composite" + steps: + # checkout commit + - name: Checkout commit + shell: bash + run: | + echo "Checkout commit" + - name: Checkout commit + uses: actions/checkout@v4.1.4 + + # get parent dir + - name: Get Parent Directory + id: parentDir + uses: ./.github/actions/get-parent-dir + + # try to get UPX + - name: Get UPX + shell: bash + run: | + echo "Get UPX" + - name: Get UPX + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + UPX_VERSION: "4.2.3" + run: | + python ./resources/ci/common/get_upx.py + + # run build.py + - name: 💬Build Binaries + shell: bash + run: | + echo "💬Build Binaries" + - name: Build Binaries + shell: bash + run: | + pip install pyinstaller + python -m source.meta.build + + # upload problem children + # - name: 🔼Upload Problem Children Artifact + # shell: bash + # run: | + # echo "🔼Upload Problem Children Artifact" + # - name: 🔼Upload Problem Children Artifact + # uses: actions/upload-artifact@v4.3.3 + # with: + # name: problemchildren-${{ inputs.os-name }}-py${{ inputs.python-version }} + # path: ./resources/app/meta/manifests/excluded_dlls.json + # if-no-files-found: ignore # 'warn' or 'ignore' are also available, defaults to `warn` + + # prepare binary artifact for later step + - name: 💬Prepare Binary Artifact + shell: bash + run: | + echo "💬Prepare Binary Artifact" + - name: Prepare Binary Artifact + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + run: | + python ./resources/ci/common/prepare_binary.py + + # upload binary artifact for later step + - name: 🔼Upload Binary Artifact + shell: bash + run: | + echo "🔼Upload Binary Artifact" + - name: 🔼Upload Binary Artifact + uses: actions/upload-artifact@v4.3.3 + with: + name: binary-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/artifact + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` diff --git a/.github/actions/get-parent-dir/action.yml b/.github/actions/get-parent-dir/action.yml new file mode 100644 index 00000000..0194f3f1 --- /dev/null +++ b/.github/actions/get-parent-dir/action.yml @@ -0,0 +1,41 @@ +name: 📁Get Parent Directory +description: Get Parent Directory + +outputs: + parentDirNotWin: + description: "Parent Directory (!Windows)" + value: ${{ steps.parentDirNotWin.outputs.value }} + parentDir: + description: "Parent Directory (Windows)" + value: ${{ steps.parentDir.outputs.value }} + +######### +# actions +######### +# mad9000/actions-find-and-replace-string@5 + +runs: + using: "composite" + steps: + # get parent directory + - name: Get Repo Name + uses: mad9000/actions-find-and-replace-string@5 + id: repoName + with: + source: ${{ github.repository }} + find: "${{ github.repository_owner }}/" + replace: "" + - name: 📁Get Parent Directory Path (!Windows) + uses: mad9000/actions-find-and-replace-string@5 + id: parentDirNotWin + with: + source: ${{ github.workspace }} + find: "${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}" + replace: ${{ steps.repoName.outputs.value }} + - name: 📁Get Parent Directory Path (Windows) + uses: mad9000/actions-find-and-replace-string@5 + id: parentDir + with: + source: ${{ steps.parentDirNotWin.outputs.value }} + find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' + replace: ${{ steps.repoName.outputs.value }} diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml new file mode 100644 index 00000000..7a062c2d --- /dev/null +++ b/.github/actions/install/action.yml @@ -0,0 +1,49 @@ +name: 💿Install +description: Install app +inputs: + calling-job: + required: true + description: Job that's calling this one + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +######### +# actions +######### +# actions/checkout@v4.1.4 +# actions/setup-python@v5.1.0 +# actions/upload-artifact@v4.3.3 + +runs: + using: "composite" + steps: + # install python + - name: 💿Install Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ inputs.python-version }} + # install modules via pip + - name: 💿Install Modules + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + run: | + echo "Install Modules" + python ./resources/ci/common/get_pipline.py + # print pipline + - name: PipLine + shell: bash + run: | + echo "PipLine" + cat ./resources/user/meta/manifests/pipline.txt + # upload pipline + - name: 🔼Upload PipLine + uses: actions/upload-artifact@v4.3.3 + with: + name: pipline-${{ inputs.calling-job }}-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./resources/user/meta/manifests + if: contains(inputs.calling-job, 'test') diff --git a/.github/actions/release-prepare/action.yml b/.github/actions/release-prepare/action.yml new file mode 100644 index 00000000..05bf9a65 --- /dev/null +++ b/.github/actions/release-prepare/action.yml @@ -0,0 +1,77 @@ +name: 📀->📦Prepare Release +description: Prepare Release for Deployment +inputs: + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +######### +# actions +######### +# Artheau/SpriteSomething/get-parent-dir +# actions/checkout@v4.1.4 +# actions/download-artifact@v4.1.7 + +runs: + using: "composite" + steps: + # checkout commit + - name: ✔️Checkout commit + shell: bash + run: | + echo "✔️Checkout commit" + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + + # get parent dir + - name: 📁Get Parent Directory + shell: bash + run: | + echo "📁Get Parent Directory" + - name: 📁Get Parent Directory + id: parentDir + uses: ./.github/actions/get-parent-dir + + # download binary artifact + - name: 🔽Download Binary Artifact + shell: bash + run: | + echo "🔽Download Binary Artifact" + - name: 🔽Download Binary Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: binary-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./ + + # download appversion artifact + - name: 🔽Download AppVersion Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: appversion + path: ${{ steps.parentDir.outputs.parentDir }}/build + + # Prepare Release + - name: 💬Prepare Release + shell: bash + run: | + echo "💬Prepare Release" + - name: Prepare Release + shell: bash + env: + OS_NAME: ${{ inputs.os-name }} + run: | + python ./resources/ci/common/prepare_release.py + + # upload archive artifact for later step + - name: 🔼Upload Archive Artifact + shell: bash + run: | + echo "🔼Upload Archive Artifact" + - name: 🔼Upload Archive Artifact + uses: actions/upload-artifact@v4.3.3 + with: + name: archive-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy diff --git a/.github/actions/tag-repo/action.yml b/.github/actions/tag-repo/action.yml new file mode 100644 index 00000000..f4c2db50 --- /dev/null +++ b/.github/actions/tag-repo/action.yml @@ -0,0 +1,76 @@ +name: 🏷️Tag Repository +description: Tag a repository + +inputs: + repository: + description: "Repository Owner/Name; octocat/Hello-World" + required: true + ref-name: + description: "Reference name; branch, tag, etc" + required: true + github-tag: + description: "Reference to tag with" + required: true + debug: + description: "Debug Mode, won't set tag" + required: false + default: "false" + +runs: + using: "composite" + steps: + - name: 🏷️Tag Repository + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ env.FINE_PAT }} + script: | + const debug = ${{ inputs.debug }} == "true" || ${{ inputs.debug }} == true; + const repository = '${{ inputs.repository }}'; + const owner = repository.substring(0,repository.indexOf('/')); + const repo = repository.substring(repository.indexOf('/')+1); + const ref = '${{ inputs.ref-name }}'; + // get git tag + const gitTag = '${{ inputs.github-tag }}'; + console.log('Repo Data: ', `${owner}/${repo}@${ref}`) + console.log('Git tag: ', gitTag) + if(gitTag == '') { + let msg = 'Result: 🔴No Git Tag sent, aborting!'; + console.log(msg) + core.setFailed(msg) + return + } + // get latest commit + const latestCommit = await github.rest.git.getRef({ + owner: owner, + repo: repo, + ref: ref + }) + // get latest refs + const latestRefs = await github.rest.git.listMatchingRefs({ + owner: owner, + repo: repo + }) + let latestTag = ''; // bucket for latest tag + // get last tag in data + for(let thisRef of latestRefs.data) { + if(thisRef['ref'].indexOf('tags') > -1) { + let refParts = thisRef['ref'].split('/'); + latestTag = refParts[-1]; + } + } + console.log('Latest tag:', latestTag) + if(latestTag != gitTag) { + if(debug) { + console.log(`DEBUG: 🔵Creating '${gitTag}' tag`) + } else { + console.log(`Result: 🟢Creating '${gitTag}' tag`) + github.rest.git.createRef({ + owner: owner, + repo: repo, + ref: `refs/tags/${gitTag}`, + sha: latestCommit.data.object.sha + }) + } + } else { + console.log('Result: 🟡Not creating release tag') + } diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 00000000..95db06db --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,97 @@ +name: ⏱️Test +description: Test app +inputs: + os-name: + required: true + description: OS to run on + python-version: + required: true + description: Python version to install + +######### +# actions +######### +# actions/checkout@v4.1.4 +# actions/download-artifact@v4.1.7 +# actions/upload-artifact@v4.3.3 +# coactions/setup-xvfb@v1.0.1 + +runs: + using: "composite" + steps: + # download pipline + - name: 🔽Download PipLine + shell: bash + run: | + echo "🔽Download PipLine" + - name: 🔽Download PipLine + uses: actions/download-artifact@v4.1.7 + with: + name: pipline-test-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./resources/user/meta/manifests + + # run tests + - name: 🖥️Test Base + shell: bash + run: | + echo "🖥️Test Base" + - name: 🖥️Test Base + shell: bash + run: | + mkdir -p ./failures + echo "" > ./failures/errors.txt + python -m pip install tqdm + python ./test/NewTestSuite.py + # - name: 🖥️Test Mystery + # shell: bash + # run: | + # echo "🖥️Test Mystery" + # if: contains(inputs.os-name, 'macos') + # - name: 🖥️Test Mystery + # shell: bash + # run: | + # python ./test/MysteryTestSuite.py + # if: contains(inputs.os-name, 'macos') + + # upload logs + - name: 🔼Upload Logs + shell: bash + run: | + echo "🔼Upload Logs" + - name: 🔼Upload Logs + uses: actions/upload-artifact@v4.3.3 + with: + name: logs-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./logs + if-no-files-found: ignore + + # print failures + - name: 💬Print Failures + if: failure() + shell: bash + run: | + echo "💬Print Failures" + - name: Print Failures + if: failure() + shell: bash + run: | + ERR_STRING="$(cat ./failures/errors.txt)" + ERR_STRING="${ERR_STRING//'%'/'%25'}" + ERR_STRING="${ERR_STRING//$'\n'/' | '}" + ERR_STRING="${ERR_STRING//$'\r'/' | '}" + ERR_STRING="${ERR_STRING//$'\n'/'%0A'}" + ERR_STRING="${ERR_STRING//$'\r'/'%0D'}" + echo "::error ::$ERR_STRING" + + # upload failures + - name: 🔼Upload Failures + if: failure() + shell: bash + run: | + echo "🔼Upload Failures" + - name: 🔼Upload Failures + if: failure() + uses: actions/upload-artifact@v4.3.3 + with: + name: failures-${{ inputs.os-name }}-py${{ inputs.python-version }} + path: ./failures diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 685c3e8d..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,317 +0,0 @@ -# workflow name -name: Build - -# fire on -on: - push: - branches: - - DoorDev - - DoorDevUnstable - 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-20.04, windows-latest ] - python-version: [ 3.9 ] -# needs: [ install-test ] - steps: - # checkout commit - - name: Checkout commit - uses: actions/checkout@v3 - # install python - - name: Install python - uses: actions/setup-python@v4 - 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 - # get parent directory - - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@3 - id: repoName - with: - source: ${{ github.repository }} - find: '${{ github.repository_owner }}/' - replace: '' - - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDirNotWin - with: - source: ${{ github.workspace }} - find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDir - with: - source: ${{ steps.parentDirNotWin.outputs.value }} - find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - # 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 ./source/meta/build-gui.py - # run build-dr.py - - name: Build DungeonRandomizer - run: | - python ./source/meta/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@v3 - with: - name: binaries-${{ matrix.os-name }} - path: ${{ steps.parentDir.outputs.value }}/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 bionic - os-name: [ ubuntu-latest, ubuntu-20.04, windows-latest ] - python-version: [ 3.9 ] - - needs: [ install-build ] - steps: - # checkout commit - - name: Checkout commit - uses: actions/checkout@v3 - # install python - - name: Install Python - uses: actions/setup-python@v4 - 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 - # get parent directory - - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@3 - id: repoName - with: - source: ${{ github.repository }} - find: '${{ github.repository_owner }}/' - replace: '' - - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDirNotWin - with: - source: ${{ github.workspace }} - find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDir - with: - source: ${{ steps.parentDirNotWin.outputs.value }} - find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - # download binary artifact - - name: Download Binary Artifact - uses: actions/download-artifact@v3 - 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@v3 - 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@v3 - with: - name: archive-${{ matrix.os-name }} - path: ${{ steps.parentDir.outputs.value }}/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: ${{ steps.parentDir.outputs.value }}/deploy - deploy-release: - name: Deploy GHReleases - runs-on: ${{ matrix.os-name }} - - # VM settings - # os & python versions - strategy: - matrix: - # release only on focal - os-name: [ ubuntu-latest ] - python-version: [ 3.9 ] - - needs: [ install-prepare-release ] - steps: - # checkout commit - - name: Checkout commit - uses: actions/checkout@v3 - # get parent directory - - name: Get Repo Name - uses: mad9000/actions-find-and-replace-string@3 - id: repoName - with: - source: ${{ github.repository }} - find: '${{ github.repository_owner }}/' - replace: '' - - name: Get Parent Directory Path (!Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDirNotWin - with: - source: ${{ github.workspace }} - find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Get Parent Directory Path (Windows) - uses: mad9000/actions-find-and-replace-string@3 - id: parentDir - with: - source: ${{ steps.parentDirNotWin.outputs.value }} - find: '${{ steps.repoName.outputs.value }}\${{ steps.repoName.outputs.value }}' - replace: ${{ steps.repoName.outputs.value }} - - name: Install Dependencies via pip - run: | - python -m pip install pytz requests - # download appversion artifact - - name: Download AppVersion Artifact - uses: actions/download-artifact@v3 - with: - name: appversion-${{ matrix.os-name }} - path: ${{ steps.parentDir.outputs.value }}/build - # download ubuntu archive artifact - - name: Download Ubuntu Archive Artifact - uses: actions/download-artifact@v3 - with: - name: archive-ubuntu-latest - path: ${{ steps.parentDir.outputs.value }}/deploy/linux - # download macos archive artifact -# - name: Download MacOS Archive Artifact -# uses: actions/download-artifact@v3 -# with: -# name: archive-macOS-latest -# path: ${{ steps.parentDir.outputs.value }}/deploy/macos - # download windows archive artifact - - name: Download Windows Archive Artifact - uses: actions/download-artifact@v3 - with: - name: archive-windows-latest - path: ${{ steps.parentDir.outputs.value }}/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}" - # create a pre/release - - name: Create a Pre/Release - id: create_release - uses: actions/create-release@v1.1.4 - 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_path: RELEASENOTES.md - 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.2 - 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-focal.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.2 -# 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.2 - 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') diff --git a/.github/workflows/release-complete.yml b/.github/workflows/release-complete.yml new file mode 100644 index 00000000..a193e0ec --- /dev/null +++ b/.github/workflows/release-complete.yml @@ -0,0 +1,47 @@ +# workflow name +name: 🏷️Tag Repositories + +# Fine-grained personal access token +# https://github.com/settings/tokens?type=beta +# token needs perms: +# actions: read/write +# commit statuses: read/write +# contents: read/write +# workflows: read/write +# copy token +# Actions secrets and variables +# github.com///settings/secrets/actions +# repository secret +# name a new secret "ALTTPER_TAGGER" +# value set to copied token + +# fire on +on: + release: + types: + - released + +jobs: + # Tag Baserom + tag-baserom: + name: 🖳Tag Baserom + runs-on: ${{ matrix.os-name }} + strategy: + matrix: + os-name: [ + # ubuntu-latest + "ubuntu-22.04" + ] + + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + - name: 🏷️Tag Repository + uses: ./.github/actions/tag-repo + env: + FINE_PAT: ${{ secrets.ALTTPER_TAGGER }} + with: + repository: ${{ github.repository_owner }}/z3randomizer + ref-name: heads/OWMain + github-tag: ${{ github.event.release.tag_name }} diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml new file mode 100644 index 00000000..3aacc0b8 --- /dev/null +++ b/.github/workflows/release-create.yml @@ -0,0 +1,418 @@ +# workflow name +name: ⏱️Test/🔨Build/🚀Deploy + +# fire on +on: [ + push, + pull_request +] + +# on: +# push: +# branches: +# - DoorDevUnstable +# - DoorDev +# - OverworldShuffleDev +# - OverworldShuffle +# pull_request: +# branches: +# - DoorDevUnstable +# - DoorDev +# - OverworldShuffleDev +# - OverworldShuffle + +# stuff to do +jobs: + # Diagnostics + diags: + # diagnostics + # call checkout + # call install python + # print python version + # call install + # call analyze github actions + # install extra python modules + # run diagnostics + name: 🧮 + runs-on: ${{ matrix.os-name }} + continue-on-error: True + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install python + - name: 💿Install Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + # print python version + - name: 🐍Python Version + shell: bash + run: | + python --version + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: diags + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call analyze github actions + - name: ⚙️Analyze used GitHub Actions + shell: bash + run: | + python ./resources/ci/common/list_actions.py + # install extra python modules + - name: 💿Install extra Python Modules + shell: bash + run: | + python -m pip install setuptools + # run diagnostics + - name: 🧮Print Diagnostics + shell: bash + run: | + python -m source.meta.run_diags + + # Test + install-test: + # test + # call checkout + # call install + # run tests + name: 💿/⏱️ + runs-on: ${{ matrix.os-name }} + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: test + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call test + - name: ⏱️Call Test + uses: ./.github/actions/test + with: + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + + # Prepare AppVersion + appversion-prepare: + # prepare appversion + # call checkout + # call install + # call appversion-prepare + name: 💬 + runs-on: ${{ matrix.os-name }} + needs: [install-test] + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: appversion-prepare + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call appversion-prepare + - name: 💬Call Prepare AppVersion + uses: ./.github/actions/appversion-prepare + + # Build + install-build: + # build + # call checkout + # call install + # call build + name: 💿/🔨 + runs-on: ${{ matrix.os-name }} + needs: [appversion-prepare] + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # call install + - name: 💿Call Install + uses: ./.github/actions/install + with: + calling-job: build + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + # call build + - name: 🔨Call Build + uses: ./.github/actions/build + with: + calling-job: build + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + + # Prepare Release + release-prepare: + # prepare release + # call checkout + # install extra python modules + # call prepare release + name: 💿/📀->📦 + runs-on: ${{ matrix.os-name }} + needs: [install-build] + continue-on-error: False + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ubuntu-20.04, + macos-latest, # macos-12 + windows-latest # windows-2022 + ] + python-version: [ "3.12" ] + steps: + # call checkout + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + # install extra python modules + - name: 💿Install extra Python Modules + shell: bash + run: | + python -m pip install setuptools + # call prepare release + - name: 📀->📦Prepare Release + uses: ./.github/actions/release-prepare + with: + os-name: ${{ matrix.os-name }} + python-version: ${{ matrix.python-version }} + + # Deploy Release + # Needs to be top-level for SECRET to work easily + release-deploy: + name: 📀->🚀 + runs-on: ${{ matrix.os-name }} + needs: [release-prepare] + + strategy: + matrix: + #TODO: OS List to run on + os-name: [ + # ubuntu-latest, # ubuntu-22.04 + ubuntu-22.04, + ] + #TODO: Python Version to run on + python-version: [ "3.12" ] + + steps: + # checkout commit + - name: ✔️Checkout commit + uses: actions/checkout@v4.1.4 + + # install extra python modules + - name: 💿Install extra Python Modules + shell: bash + run: | + python -m pip install pytz requests + + # get parent dir + - name: 📁Get Parent Directory + id: parentDir + uses: ./.github/actions/get-parent-dir + + # download appversion artifact + - name: 🔽Download AppVersion Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: appversion + path: ${{ steps.parentDir.outputs.parentDir }}/build + + # download ubuntu archive artifact + - name: 🔽Download Ubuntu Archive Artifact + uses: actions/download-artifact@v4.1.7 + with: + # should run on latest explicit ubuntu version + name: archive-ubuntu-22.04-py${{ matrix.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy/linux + + # download macos archive artifact + - name: 🔽Download MacOS Archive Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: archive-macos-latest-py${{ matrix.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy/macos + + # download windows archive artifact + - name: 🔽Download Windows Archive Artifact + uses: actions/download-artifact@v4.1.7 + with: + name: archive-windows-latest-py${{ matrix.python-version }} + path: ${{ steps.parentDir.outputs.parentDir }}/deploy/windows + + # determine linux archive asset + - name: ❔Identify Linux Archive Asset + id: identify-linux-asset + shell: bash + run: | + ASSET_LINUX="$(ls ${{ steps.parentDir.outputs.parentDir }}/deploy/linux)" + echo "asset_linux=$ASSET_LINUX" >> $GITHUB_OUTPUT + + # determine macos archive asset + - name: ❔Identify MacOS Archive Asset + id: identify-macos-asset + shell: bash + run: | + ASSET_MACOS="$(ls ${{ steps.parentDir.outputs.parentDir }}/deploy/macos)" + echo "asset_macos=$ASSET_MACOS" >> $GITHUB_OUTPUT + + # determine windows archive asset + - name: ❔Identify Windows Archive Asset + id: identify-windows-asset + shell: bash + run: | + ASSET_WIN="$(ls ${{ steps.parentDir.outputs.parentDir }}/deploy/windows)" + echo "asset_windows=$ASSET_WIN" >> $GITHUB_OUTPUT + + # archive listing + # - name: Archive Listing + # shell: bash + # run: | + # ls -R ${{ steps.parentDir.outputs.parentDir }}/deploy/ + + # debug info + #TODO: Project Name + - name: 📝Debug Info + id: debug_info + run: | + PROJECT_NAME="ALttPDoorRandomizer" + echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT + + GITHUB_TAG="$(head -n 1 ../build/app_version.txt)" + echo "github_tag=$GITHUB_TAG" >> $GITHUB_OUTPUT + + RELEASE_NAME="${PROJECT_NAME} ${GITHUB_TAG}" + echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT + + ASSET_PREFIX="${PROJECT_NAME}-${GITHUB_TAG}" + echo "asset_prefix=$ASSET_PREFIX" >> $GITHUB_OUTPUT + + echo "Project Name: ${PROJECT_NAME}" + echo "Release Name: ${RELEASE_NAME}" + echo "Asset Prefix: ${ASSET_PREFIX}" + echo "Git Tag: ${GITHUB_TAG}" + echo "Linux Asset: ${{ steps.identify-linux-asset.outputs.asset_linux }}" + echo "MacOS Asset: ${{ steps.identify-macos-asset.outputs.asset_macos }}" + echo "Windows Asset: ${{ steps.identify-windows-asset.outputs.asset_windows }}" + + # create a release (MASTER) + #TODO: Make sure we updated RELEASENOTES.md + #TODO: Make sure we're firing on the proper branches + # if: contains(github.ref, 'master') # branch or tag name + # if: contains(github.event.head_commit.message, 'Version bump') # commit message + - name: 📀->🚀Create a Release (MASTER) + id: create_release + uses: actions/create-release@v1.1.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.debug_info.outputs.github_tag }} + release_name: ${{ steps.debug_info.outputs.release_name }} + body_path: RELEASENOTES.md + # draft: true + if: contains(github.ref, 'master') + + # upload linux archive asset (MASTER) + #TODO: Make sure we're firing on the proper branches + - name: 🔼Upload Linux Archive Asset (MASTER) + id: upload-linux-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/linux/${{ steps.identify-linux-asset.outputs.asset_linux }} + asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-linux-focal.tar.gz + asset_content_type: application/gzip + if: contains(github.ref, 'master') + + # upload macos archive asset (MASTER) + #TODO: Make sure we're firing on the proper branches + - name: 🔼Upload MacOS Archive Asset (MASTER) + id: upload-macos-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/macos/${{ steps.identify-macos-asset.outputs.asset_macos }} + asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-osx.tar.gz + asset_content_type: application/gzip + if: contains(github.ref, 'master') + + # upload windows archive asset (MASTER) + #TODO: Make sure we're firing on the proper branches + - name: 🔼Upload Windows Archive Asset (MASTER) + id: upload-windows-asset + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/windows/${{ steps.identify-windows-asset.outputs.asset_windows }} + asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-windows.zip + asset_content_type: application/zip + if: contains(github.ref, 'master') diff --git a/.gitignore b/.gitignore index 44e00b30..bc4845cf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ *.srm *.bst *.wixobj -build +/build bundle/components.wxs dist README.html @@ -39,6 +39,10 @@ resources/user/* get-pip.py venv +/test test_games/ data/sprites/official/selan.1.zspr *.zspr + +*errors.txt +*success.txt diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index cf0f73bc..94b83faf 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -23,7 +23,7 @@ def start(): # print diagnostics # usage: py DungeonRandomizer.py --diags if args.diags: - diags = diagnostics.output(__version__) + diags = diagnostics.output() print("\n".join(diags)) sys.exit(0) diff --git a/Text.py b/Text.py index f2554030..052defaf 100644 --- a/Text.py +++ b/Text.py @@ -2,6 +2,8 @@ from collections import OrderedDict import logging import re +import warnings +warnings.filterwarnings("ignore", category=SyntaxWarning) text_addresses = {'Pedestal': (0x180300, 256), 'Triforce': (0x180400, 256), @@ -644,7 +646,7 @@ class MultiByteCoreTextMapper(object): linespace = wrap line = lines.pop(0) - match = re.search('^\{[A-Z0-9_:]+\}$', line) + match = re.search(r'^\{[A-Z0-9_:]+\}$', line) if match: if line == '{PAGEBREAK}': if lineindex % 3 != 0: @@ -663,13 +665,13 @@ class MultiByteCoreTextMapper(object): while words: word = words.pop(0) - match = re.search('^(\{[A-Z0-9_:]+\}).*', word) + match = re.search(r'^(\{[A-Z0-9_:]+\}).*', word) if match: start_command = match.group(1) outbuf.extend(cls.special_commands[start_command]) word = word.replace(start_command, '') - match = re.search('(\{[A-Z0-9_:]+\})\.?$', word) + match = re.search(r'(\{[A-Z0-9_:]+\})\.?$', word) if match: end_command = match.group(1) word = word.replace(end_command, '') diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index 8d54f848..64129bad 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -107,8 +107,8 @@ compass_shuffle: on: 1 off: 1 smallkey_shuffle: - on: 1 - off: 1 + wild: 1 + none: 1 bigkey_shuffle: on: 1 off: 1 diff --git a/resources/app/meta/manifests/app_version.txt b/resources/app/meta/manifests/app_version.txt new file mode 100644 index 00000000..e69de29b diff --git a/resources/app/meta/manifests/binaries.json b/resources/app/meta/manifests/binaries.json new file mode 100644 index 00000000..620df72f --- /dev/null +++ b/resources/app/meta/manifests/binaries.json @@ -0,0 +1,7 @@ +[ + "DungeonRandomizer", + "Gui", + "MultiClient", + "MultiServer", + "Mystery" +] diff --git a/resources/app/meta/manifests/excluded_dlls.json b/resources/app/meta/manifests/excluded_dlls.json new file mode 100644 index 00000000..825d5999 --- /dev/null +++ b/resources/app/meta/manifests/excluded_dlls.json @@ -0,0 +1,34 @@ +[ + "conio", + "console", + "convert", + "datetime", + "debug", + "environment", + "errorhandling", + "file", + "filesystem", + "handle", + "heap", + "interlocked", + "libraryloader", + "locale", + "localization", + "math", + "memory", + "namedpipe", + "process", + "processenvironment", + "processthreads", + "profile", + "rtlsupport", + "runtime", + "stdio", + "string", + "synch", + "sysinfo", + "time", + "timezone", + "util", + "utility" +] diff --git a/resources/app/meta/manifests/pip_requirements.txt b/resources/app/meta/manifests/pip_requirements.txt index faa3c48f..26bede61 100644 --- a/resources/app/meta/manifests/pip_requirements.txt +++ b/resources/app/meta/manifests/pip_requirements.txt @@ -1,7 +1,8 @@ aenum +aioconsole +colorama +distro fast-enum python-bps-continued -colorama -aioconsole +pyyaml websockets -pyyaml \ No newline at end of file diff --git a/resources/ci/common/common.py b/resources/ci/common/common.py index b89a266b..5329c768 100644 --- a/resources/ci/common/common.py +++ b/resources/ci/common/common.py @@ -1,6 +1,11 @@ import os # for env vars import stat # file statistics import sys # default system info +try: + import distro +except ModuleNotFoundError as e: + pass + from my_path import get_py_path global UBUNTU_VERSIONS @@ -8,15 +13,20 @@ global DEFAULT_EVENT global DEFAULT_REPO_SLUG global FILENAME_CHECKS global FILESIZE_CHECK -UBUNTU_VERSIONS = { - "latest": "focal", - "20.04": "focal", - "18.04": "bionic", - "16.04": "xenial" -} +# GitHub Hosted Runners +# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories +# ubuntu: 22.04, 20.04 +# windows: 2022, 2019 +# macos: 14, 13, 12, 11 DEFAULT_EVENT = "event" DEFAULT_REPO_SLUG = "miketrethewey/ALttPDoorRandomizer" -FILENAME_CHECKS = [ "Gui", "DungeonRandomizer" ] +FILENAME_CHECKS = [ + "DungeonRandomizer", + "Gui", + "MultiClient", + "MultiServer", + "Mystery" +] FILESIZE_CHECK = (6 * 1024 * 1024) # 6MB # take number of bytes and convert to string with units measure @@ -38,12 +48,19 @@ def prepare_env(): global DEFAULT_REPO_SLUG env = {} - # get app version + # 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() + APP_VERSION_FILES = [ + os.path.join(".","resources","app","meta","manifests","app_version.txt"), + os.path.join("..","build","app_version.txt") + ] + for app_version_file in APP_VERSION_FILES: + if os.path.isfile(app_version_file): + with open(app_version_file,"r") as f: + lines = f.readlines() + if len(lines) > 0: + APP_VERSION = lines[0].strip() + # ci data env["CI_SYSTEM"] = os.getenv("CI_SYSTEM","") # py data @@ -96,9 +113,11 @@ def prepare_env(): 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 in UBUNTU_VERSIONS: - OS_VERSION = UBUNTU_VERSIONS[OS_VERSION] - OS_DIST = OS_VERSION + try: + if distro.codename() != "": + OS_DIST = distro.codename() + except NameError as e: + pass if OS_VERSION == "" and not OS_DIST == "" and not OS_DIST == "notset": OS_VERSION = OS_DIST @@ -111,7 +130,7 @@ def prepare_env(): # if the app version didn't have the build number, add it # set to . if env["BUILD_NUMBER"] not in GITHUB_TAG: - GITHUB_TAG += '.' + env["BUILD_NUMBER"] + GITHUB_TAG += ".r" + env["BUILD_NUMBER"] env["GITHUB_TAG"] = GITHUB_TAG env["OS_NAME"] = OS_NAME diff --git a/resources/ci/common/get_get_pip.py b/resources/ci/common/get_get_pip.py index a0e127ba..1de2898b 100644 --- a/resources/ci/common/get_get_pip.py +++ b/resources/ci/common/get_get_pip.py @@ -10,7 +10,7 @@ def get_get_pip(PY_VERSION): try: import pip except ImportError: - print("Getting pip getter!") + print("🟡Getting pip getter!") #make the request! url = "https://bootstrap.pypa.io/get-pip.py" context = ssl._create_unverified_context() @@ -40,7 +40,7 @@ def get_get_pip(PY_VERSION): if float(PY_VERSION) > 0: PYTHON_EXECUTABLE = "py" - print("Getting pip!") + print("🟡Getting pip!") args = [ env["PYTHON_EXE_PATH"] + PYTHON_EXECUTABLE, '-' + str(PY_VERSION), @@ -58,6 +58,6 @@ if __name__ == "__main__": try: import pip - print("pip is installed") + print("🟢pip is installed") except ImportError: get_get_pip(PY_VERSION) diff --git a/resources/ci/common/get_pipline.py b/resources/ci/common/get_pipline.py new file mode 100644 index 00000000..edc5714a --- /dev/null +++ b/resources/ci/common/get_pipline.py @@ -0,0 +1,440 @@ +# import modules +import common # app common functions + +import json # json manipulation +import os # for os data, filesystem manipulation +import subprocess # for running shell commands +import sys # for system commands +import traceback # for errors + +# get env +env = common.prepare_env() # get environment variables + +# width for labels +WIDTH = 70 + +# bucket for cli args +args = [] + +# pip exe path +PIPEXE = "" + +# py exe path +# py version +# py minor version +PYTHON_EXECUTABLE = os.path.splitext(sys.executable.split(os.path.sep).pop())[0] # get command to run python +PYTHON_VERSION = sys.version.split(" ")[0] +PYTHON_MINOR_VERSION = '.'.join(PYTHON_VERSION.split(".")[:2]) + +# pip string version +# pip float version +PIP_VERSION = "" +PIP_FLOAT_VERSION = 0 + +# success +SUCCESS = False +# bucket for versions +VERSIONS = {} + +# process module output +# read output from installing +# print relevant info +# print unknown stuff +def process_module_output(lines): + for line in lines: + # if there's an error, print it and bail + if "status 'error'" in line.strip(): + print( + "🔴[%s] %s" + % + ( + "_", + line.strip() + ) + ) + return + # sys.exit(1) + # if it's already satisfied or building a wheel, print version data + elif "already satisfied" in line or \ + "Building wheel" in line or \ + "Created wheel" in line: + + modulename = print_module_line(line) + + if "=" not in modulename and VERSIONS[modulename]["installed"] != VERSIONS[modulename]["latest"]: + # install modules from list + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "--upgrade", + f"{modulename}" + ], + capture_output=True, + text=True + ) + # if there's output + if ret.stdout.strip(): + process_module_output(ret.stdout.strip().split("\n")) + + # ignore lines about certain things + elif "Attempting uninstall" in line or \ + "Collecting" in line or \ + "Downloading" in line or \ + "eta 0:00:00" in line or \ + "Found existing" in line or \ + "Installing collected" in line or \ + "Preparing metadata" in line or \ + "Successfully built" in line or \ + "Successfully installed" in line or \ + "Successfully uninstalled" in line or \ + "Stored in" in line or \ + "Uninstalling " in line or \ + "Using cached" in line: + pass + # else, I don't know what it is, print it + else: + print(line.strip()) + print("") + +# print module line +# name, installed version, latest version +def print_module_line(line): + global VERSIONS + # is it already installed? + satisfied = line.strip().split(" in ") + # get the installed version + sver = ((len(satisfied) > 1) and satisfied[1].split("(").pop().replace(")", "")) or "" + + # if we're making a wheel + if "Created wheel" in line: + line = line.strip().split(':') + satisfied = [line[0]] + sver = line[1].split('-')[1] + + # get module name + modulename = satisfied[0].replace("Requirement already satisfied: ", "") + # save info for later use + VERSIONS[modulename] = { + "installed": sver, + "latest": (sver and get_module_version(satisfied[0].split(" ")[-1])).strip() or "" + } + + # print what we found + print( + ( + "[%s] %s\t%s\t%s" + % + ( + "Building wheel" in line and '.' or "X", + satisfied[0].ljust(len("Requirement already satisfied: ") + len("python-bps-continued")), + VERSIONS[modulename]["installed"], + VERSIONS[modulename]["latest"] + ) + ) + ) + # return the name of this module + return modulename + +# get module version +# get installed version +def get_module_version(module): + # pip index versions [module] // >= 21.2 + # pip install [module]== // >= 21.1 + # pip install --use-deprecated=legacy-resolver [module]== // >= 20.3 + # pip install [module]== // >= 9.0 + # pip install [module]==blork // < 9.0 + global args + global PIPEXE + global PIP_FLOAT_VERSION + ret = "" + ver = "" + + # based on version of pip, get the installation status of a module + if float(PIP_FLOAT_VERSION) >= 21.2: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "index", + "versions", + module + ], + capture_output=True, + text=True + ) + lines = ret.stdout.strip().split("\n") + lines = lines[2::] + vers = (list(map(lambda x: x.split(' ')[-1], lines))) + if len(vers) > 1: + ver = vers[1] + elif float(PIP_FLOAT_VERSION) >= 21.1: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + f"{module}==" + ], + capture_output=True, + text=True + ) + elif float(PIP_FLOAT_VERSION) >= 20.3: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "--use-deprecated=legacy-resolver", + f"{module}==" + ], + capture_output=True, + text=True + ) + elif float(PIP_FLOAT_VERSION) >= 9.0: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + f"{module}==" + ], + capture_output=True, + text=True + ) + elif float(PIP_FLOAT_VERSION) < 9.0: + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + f"{module}==blork" + ], + capture_output=True, + ext=True + ) + + # if ver == "" and ret.stderr.strip(): + # ver = (ret.stderr.strip().split("\n")[0].split(",")[-1].replace(')', '')).strip() + + # return what we found + return ver + +# get python info +def python_info(): + global args + global PYTHON_VERSION + + # get python debug info + ret = subprocess.run([*args, "--version"], capture_output=True, text=True) + if ret.stdout.strip(): + PYTHON_VERSION = ret.stdout.strip().split(" ")[1] + PY_STRING = ( + "%s\t%s\t%s" + % + ( + ((isinstance(args[0], list) and " ".join( + args[0])) or args[0]).strip(), + PYTHON_VERSION, + sys.platform + ) + ) + print(PY_STRING) + print('.' * WIDTH) + +# get pip info +def pip_info(): + global args + global PIPEXE + global PIPEXE + global VERSIONS + + # get pip debug info + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "--version" + ], + capture_output=True, + text=True + ) + if ret.stdout.strip(): + if " from " in ret.stdout.strip(): + PIP_VERSION = ret.stdout.strip().split(" from ")[0].split(" ")[1] + if PIP_VERSION: + b, f, a = PIP_VERSION.partition('.') + global PIP_FLOAT_VERSION + PIP_FLOAT_VERSION = b+f+a.replace('.', '') + PIP_LATEST = get_module_version("pip") + + VERSIONS["py"] = { + "version": PYTHON_VERSION, + "platform": sys.platform + } + VERSIONS["pip"] = { + "version": [ + PIP_VERSION, + PIP_FLOAT_VERSION + ], + "latest": PIP_LATEST + } + + PIP_STRING = ( + "%s\t%s\t%s\t%s\t%s\t%s" + % + ( + ((isinstance(args[0], list) and " ".join( + args[0])) or args[0]).strip(), + PYTHON_VERSION, + sys.platform, + PIPEXE, + PIP_VERSION, + PIP_LATEST + ) + ) + print(PIP_STRING) + print('.' * WIDTH) + +# upgrade pip +def pip_upgrade(): + global args + global PIPEXE + + # upgrade pip + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "--upgrade", "pip" + ], + capture_output=True, + text=True + ) + # get output + if ret.stdout.strip(): + # if it's not already satisfied, update it + if "already satisfied" not in ret.stdout.strip(): + print(ret.stdout.strip()) + pip_info() + +# install modules +def install_modules(): + global args + global PIPEXE + global SUCCESS + + # install modules from list + ret = subprocess.run( + [ + *args, + "-m", + PIPEXE, + "install", + "-r", + os.path.join( + ".", + "resources", + "app", + "meta", + "manifests", + "pip_requirements.txt" + ) + ], + capture_output=True, + text=True + ) + + # if there's output + if ret.stdout.strip(): + process_module_output(ret.stdout.strip().split("\n")) + manifests_path = os.path.join(".", "resources", "user", "meta", "manifests") + if not os.path.isdir(manifests_path): + os.makedirs(manifests_path) + + with open(os.path.join(manifests_path, "settings.json"), "w+") as settings: + settings.write( + json.dumps( + { + "py": args, + "pip": PIPEXE, + "pipline": " ".join(args) + " -m " + PIPEXE, + "versions": VERSIONS + }, + indent=2 + ) + ) + with open(os.path.join(manifests_path, "pipline.txt"), "w+") as settings: + settings.write(" ".join(args) + " -m " + PIPEXE) + SUCCESS = True + + +def main(): + global args + global PIPEXE + global SUCCESS + # print python debug info + heading = ( + "%s-%s-%s" + % + ( + PYTHON_EXECUTABLE, + PYTHON_VERSION, + sys.platform + ) + ) + print(heading) + print('=' * WIDTH) + + # figure out pip executable + PIPEXE = "pip" if "windows" in env["OS_NAME"] else "pip3" + PIPEXE = "pip" if "osx" in env["OS_NAME"] and "actions" in env["CI_SYSTEM"] else PIPEXE + + PIP_VERSION = "" # holder for pip's version + + SUCCESS = False + # foreach py executable + for PYEXE in ["py", "python3", "python"]: + if SUCCESS: + continue + + args = [] + # if it's the py launcher, specify the version + if PYEXE == "py": + PYEXE = [PYEXE, "-" + PYTHON_MINOR_VERSION] + # if it ain't windows, skip it + if "windows" not in env["OS_NAME"]: + continue + + # build executable command + if isinstance(PYEXE, list): + args = [*PYEXE] + else: + args = [PYEXE] + + try: + python_info() + + # foreach pip executable + for PIPEXE in ["pip3", "pip"]: + pip_info() + pip_upgrade() + install_modules() + + # if something else went fucky, print it + except Exception as e: + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/resources/ci/common/get_upx.py b/resources/ci/common/get_upx.py index 73c6b6f3..16982aaf 100644 --- a/resources/ci/common/get_upx.py +++ b/resources/ci/common/get_upx.py @@ -21,10 +21,11 @@ if not os.path.isdir(os.path.join(".","upx")): UPX_FILE = UPX_SLUG + ".tar.xz" UPX_URL = "https://github.com/upx/upx/releases/download/v" + UPX_VERSION + '/' + UPX_FILE + # if it's not macos if "osx" not in env["OS_NAME"]: - print("Getting UPX: " + UPX_FILE) + # download UPX with open(os.path.join(".",UPX_FILE),"wb") as upx: UPX_REQ = urllib.request.Request( UPX_URL, @@ -34,8 +35,10 @@ if not os.path.isdir(os.path.join(".","upx")): UPX_DATA = UPX_REQ.read() upx.write(UPX_DATA) + # extract UPX unpack_archive(UPX_FILE,os.path.join(".")) + # move UPX os.rename(os.path.join(".",UPX_SLUG),os.path.join(".","upx")) os.remove(os.path.join(".",UPX_FILE)) diff --git a/resources/ci/common/list_actions.py b/resources/ci/common/list_actions.py new file mode 100644 index 00000000..4aab7812 --- /dev/null +++ b/resources/ci/common/list_actions.py @@ -0,0 +1,168 @@ +# pylint: disable=invalid-name +''' +List GitHub Actions versions used and latest versions +''' +import json +import os +import ssl +import urllib.request +import yaml +from json.decoder import JSONDecodeError + +allACTIONS = {} +listACTIONS = [] + +VER_WIDTH = 10 +NAME_WIDTH = 40 +LINE_WIDTH = 1 + NAME_WIDTH + 5 + VER_WIDTH + 5 + VER_WIDTH + 1 + +def process_walk(key, node): + ''' + Process walking through the array + ''' + global allACTIONS + global listACTIONS + if key == "uses": + action = node.split('@') + version = "" + if '@' in node: + version = action[1] + action = action[0] + if action not in allACTIONS: + allACTIONS[action] = { + "versions": [], + "latest": "" + } + allACTIONS[action]["versions"].append(version) + allACTIONS[action]["versions"] = list( + set( + allACTIONS[action]["versions"] + ) + ) + listACTIONS.append(node) + + +def walk(key, node): + ''' + How to walk through the array + ''' + if isinstance(node, dict): + return {k: walk(k, v) for k, v in node.items()} + elif isinstance(node, list): + return [walk(key, x) for x in node] + else: + return process_walk(key, node) + + +for r, d, f in os.walk(os.path.join(".", ".github")): + if "actions" in r or "workflows" in r: + for filename in f: + # if it's not a YAML or it's turned off, skip it + if (".yml" not in filename and ".yaml" not in filename) or (".off" in filename): + continue + listACTIONS = [] + # print filename + filename_line = "-" * (len(os.path.join(r, filename)) + 2) + print( + " " + + filename_line + + " " + ) + print("| " + os.path.join(r, filename) + " |") + # read the file + with(open(os.path.join(r, filename), "r", encoding="utf-8")) as yamlFile: + print( + "|" + + filename_line + + "-" + + ("-" * (LINE_WIDTH - len(filename_line) + 1)) + + " " + ) + yml = yaml.safe_load(yamlFile) + walk("uses", yml) + dictACTIONS = {} + for k in sorted(list(set(listACTIONS))): + action = k.split('@')[0] + version = k.split('@')[1] if '@' in k else "" + latest = "" + # if it's not a location action, get the latest version number + if "./." not in action: + apiURL = f"https://api.github.com/repos/{action}/releases/latest" + if True: + apiReq = None + try: + apiReq = urllib.request.urlopen( + apiURL, + context=ssl._create_unverified_context() + ) + except urllib.error.HTTPError as e: + if e.code != 403: + print(e.code, apiURL) + if apiReq: + apiRes = {} + try: + apiRes = json.loads( + apiReq.read().decode("utf-8")) + except JSONDecodeError as e: + raise ValueError("🔴API Request failed: " + apiURL) + if apiRes: + latest = apiRes["tag_name"] if "tag_name" in apiRes else "" + if latest != "": + allACTIONS[action]["latest"] = latest + dictACTIONS[action] = version + # print action name and version info + for action, version in dictACTIONS.items(): + print( + "| " + \ + f"{action.ljust(NAME_WIDTH)}" + \ + "\t" + \ + f"{(version or 'N/A').ljust(VER_WIDTH)}" + \ + "\t" + \ + f"{(allACTIONS[action]['latest'] or 'N/A').ljust(VER_WIDTH)}" + \ + " |" + ) + print( + " " + + ("-" * (LINE_WIDTH + 2)) + + " " + ) + print("") + +# print outdated versions summary +first = True +outdated = False +for action, actionData in allACTIONS.items(): + if len(actionData["versions"]) > 0: + if actionData["latest"] != "" and actionData["versions"][0] != actionData["latest"]: + outdated = True + if first: + first = False + filename_line = "-" * (len("| Outdated |")) + print( + " " + + filename_line + + " " + ) + print("| 🔴Outdated |") + print( + "|" + + filename_line + + "-" + + ("-" * (LINE_WIDTH - len(filename_line) + 1)) + + " " + ) + print( + "| " + \ + f"{action.ljust(40)}" + \ + "\t" + \ + f"{(','.join(actionData['versions']) or 'N/A').ljust(10)}" + \ + "\t" + \ + f"{actionData['latest'].ljust(10)}" + \ + " |" + ) +if outdated: + print( + " " + + ("-" * (LINE_WIDTH + 2)) + + " " + ) diff --git a/resources/ci/common/prepare_appversion.py b/resources/ci/common/prepare_appversion.py index bd26318e..0f413298 100644 --- a/resources/ci/common/prepare_appversion.py +++ b/resources/ci/common/prepare_appversion.py @@ -5,12 +5,12 @@ 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 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")) diff --git a/resources/ci/common/prepare_binary.py b/resources/ci/common/prepare_binary.py index ff9b7c99..4d9ac5e4 100644 --- a/resources/ci/common/prepare_binary.py +++ b/resources/ci/common/prepare_binary.py @@ -1,42 +1,48 @@ -import distutils.dir_util # for copying trees +""" +Locate and prepare binary builds +""" +# 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 stat # for file stats +# import subprocess # do stuff at the shell level import common -from shutil import copy, make_archive, move, rmtree # file manipulation +from shutil import move # 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")) + 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")) + BUILD_FILENAME = common.find_binary(os.path.join("..","artifact")) if isinstance(BUILD_FILENAME,str): - BUILD_FILENAME = list(BUILD_FILENAME) + BUILD_FILENAME = list(BUILD_FILENAME) BUILD_FILENAMES = BUILD_FILENAME +print("OS Name: " + env["OS_NAME"]) +print("OS Version: " + env["OS_VERSION"]) +print("OS Distribution: " + env["OS_DIST"]) +print("") for BUILD_FILENAME in BUILD_FILENAMES: - DEST_FILENAME = common.prepare_filename(BUILD_FILENAME) + 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)) - else: - exit(1) + print("Build Filename: " + BUILD_FILENAME) + print("Dest Filename: " + DEST_FILENAME) + if not BUILD_FILENAME == "": + print("Build Filesize: " + common.file_size(BUILD_FILENAME)) + else: + exit(1) - if not BUILD_FILENAME == "": - move( - os.path.join(".",BUILD_FILENAME), - os.path.join("..","artifact",BUILD_FILENAME) - ) + if not BUILD_FILENAME == "": + move( + os.path.join(".",BUILD_FILENAME), + os.path.join("..","artifact",BUILD_FILENAME) + ) + print("") diff --git a/resources/ci/common/prepare_release.py b/resources/ci/common/prepare_release.py index c3869f2d..fea373b5 100644 --- a/resources/ci/common/prepare_release.py +++ b/resources/ci/common/prepare_release.py @@ -71,7 +71,7 @@ if len(BUILD_FILENAMES) > 0: # clean the git slate git_clean() - # mv dirs from source code + # mv dirs from source code dirs = [ os.path.join(".",".git"), os.path.join(".",".github"), @@ -98,8 +98,8 @@ if len(BUILD_FILENAMES) > 0: 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 if windows + # .tar.gz otherwise if len(BUILD_FILENAMES) > 1: ZIP_FILENAME = os.path.join("..","deploy",env["REPO_NAME"]) else: @@ -111,7 +111,7 @@ if len(BUILD_FILENAMES) > 0: make_archive(ZIP_FILENAME,"gztar") ZIP_FILENAME += ".tar.gz" - # mv dirs back + # mv dirs back for dir in dirs: if os.path.isdir(os.path.join("..","build",dir)): move( @@ -124,15 +124,15 @@ for BUILD_FILENAME in BUILD_FILENAMES: print("Build Filename: " + BUILD_FILENAME) print("Build Filesize: " + common.file_size(BUILD_FILENAME)) else: - print("No Build to prepare: " + BUILD_FILENAME) + 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("🟡No Zip to prepare: " + ZIP_FILENAME) -print("Git tag: " + env["GITHUB_TAG"]) +print("App Version: " + env["GITHUB_TAG"]) if (len(BUILD_FILENAMES) == 0) or (ZIP_FILENAME == ""): exit(1) diff --git a/source/DungeonRandomizer.spec b/source/DungeonRandomizer.spec deleted file mode 100644 index beb2ecfc..00000000 --- a/source/DungeonRandomizer.spec +++ /dev/null @@ -1,68 +0,0 @@ -# -*- mode: python -*- - -import sys - -block_cipher = None -console = True # <--- change this to True to enable command prompt when the app runs - -if sys.platform.find("mac") or sys.platform.find("osx"): - console = False - -BINARY_SLUG = "DungeonRandomizer" - -def recurse_for_py_files(names_so_far): - returnvalue = [] - for name in os.listdir(os.path.join(*names_so_far)): - if name != "__pycache__": - subdir_name = os.path.join(*names_so_far, name) - if os.path.isdir(subdir_name): - new_name_list = names_so_far + [name] - for filename in os.listdir(os.path.join(*new_name_list)): - base_file,file_extension = os.path.splitext(filename) - if file_extension == ".py": - new_name = ".".join(new_name_list+[base_file]) - if not new_name in returnvalue: - returnvalue.append(new_name) - returnvalue.extend(recurse_for_py_files(new_name_list)) - returnvalue.append("PIL._tkinter_finder") #Linux needs this - return returnvalue - -hiddenimports = [] -binaries = [] - -a = Analysis([f"../{BINARY_SLUG}.py"], - pathex=[], - binaries=binaries, - datas=[('../data/', 'data/')], - hiddenimports=hiddenimports, - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) - -# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller -excluded_binaries = [ - 'VCRUNTIME140.dll', - 'ucrtbase.dll', - 'msvcp140.dll', - 'mfc140u.dll'] -a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name=BINARY_SLUG, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - runtime_tmpdir=None, - console=console ) diff --git a/source/Gui.spec b/source/Gui.spec deleted file mode 100644 index 8b140a75..00000000 --- a/source/Gui.spec +++ /dev/null @@ -1,69 +0,0 @@ -# -*- mode: python -*- - -import sys - -block_cipher = None -console = True # <--- change this to True to enable command prompt when the app runs - -if sys.platform.find("mac") or sys.platform.find("osx"): - console = False - -BINARY_SLUG = "Gui" - -def recurse_for_py_files(names_so_far): - returnvalue = [] - for name in os.listdir(os.path.join(*names_so_far)): - if name != "__pycache__": - subdir_name = os.path.join(*names_so_far, name) - if os.path.isdir(subdir_name): - new_name_list = names_so_far + [name] - for filename in os.listdir(os.path.join(*new_name_list)): - base_file,file_extension = os.path.splitext(filename) - if file_extension == ".py": - new_name = ".".join(new_name_list+[base_file]) - if not new_name in returnvalue: - returnvalue.append(new_name) - returnvalue.extend(recurse_for_py_files(new_name_list)) - returnvalue.append("PIL._tkinter_finder") #Linux needs this - return returnvalue - -hiddenimports = [] -binaries = [] - -a = Analysis([f"../{BINARY_SLUG}.py"], - pathex=[], - binaries=binaries, - datas=[('../data/', 'data/')], - hiddenimports=hiddenimports, - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) - -# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller -excluded_binaries = [ - 'VCRUNTIME140.dll', - 'ucrtbase.dll', - 'msvcp140.dll', - 'mfc140u.dll'] -a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name=BINARY_SLUG, - debug=False, - bootloader_ignore_signals=False, - icon='../data/ER.ico', - strip=False, - upx=True, - runtime_tmpdir=None, - console=console ) diff --git a/source/Template.spec b/source/Template.spec new file mode 100644 index 00000000..87fb999a --- /dev/null +++ b/source/Template.spec @@ -0,0 +1,98 @@ +# -*- mode: python -*- + +import json +import os +import sys +from json.decoder import JSONDecodeError +from PyInstaller.utils.hooks import collect_submodules + +block_cipher = None +console = False # <--- change this to True to enable command prompt when the app runs + +if sys.platform.find("mac") or sys.platform.find("osx"): + console = True + +BINARY_SLUG = "" + + +def recurse_for_py_files(names_so_far): + # get py files + returnvalue = [] + for name in os.listdir(os.path.join(*names_so_far)): + # ignore __pycache__ + if name != "__pycache__": + subdir_name = os.path.join(*names_so_far, name) + if os.path.isdir(subdir_name): + new_name_list = names_so_far + [name] + for filename in os.listdir(os.path.join(*new_name_list)): + base_file, file_extension = os.path.splitext(filename) + # if it's a .py + if file_extension == ".py": + new_name = ".".join(new_name_list+[base_file]) + if not new_name in returnvalue: + returnvalue.append(new_name) + returnvalue.extend(recurse_for_py_files(new_name_list)) + return returnvalue + + +hiddenimports = recurse_for_py_files(["source"]) +for hidden in (collect_submodules("pkg_resources")): + hiddenimports.append(hidden) + +a = Analysis( + [f"../{BINARY_SLUG}.py"], + pathex=[], + binaries=[], + datas=[('../data/', 'data/')], + hiddenimports=hiddenimports, + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False +) + +# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller +excluded_binaries = [ + 'mfc140u.dll', + 'msvcp140.dll', + 'ucrtbase.dll', + 'VCRUNTIME140.dll' +] + +# win is temperamental +with open(os.path.join(".","resources","app","meta","manifests","excluded_dlls.json")) as dllsManifest: + dlls = [] + try: + dlls = json.load(dllsManifest) + except JSONDecodeError as e: + raise ValueError("Windows DLLs manifest malformed!") + for dll in dlls: + for submod in ["core", "crt"]: + for ver in ["1-1-0", "1-1-1", "1-2-0", "2-1-0"]: + excluded_binaries.append(f"api-ms-win-{submod}-{dll}-l{ver}.dll") + +a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries]) + +pyz = PYZ( + a.pure, + a.zipped_data, + cipher=block_cipher +) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name=BINARY_SLUG, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=console +) diff --git a/source/classes/appversion.py b/source/classes/appversion.py new file mode 100644 index 00000000..2d331c15 --- /dev/null +++ b/source/classes/appversion.py @@ -0,0 +1,17 @@ +import os + +from Main import __version__ +DR_VERSION = __version__ + +def write_appversion(): + APP_VERSION = DR_VERSION + if "-" in APP_VERSION: + APP_VERSION = APP_VERSION[:APP_VERSION.find("-")] + APP_VERSION_FILE = os.path.join(".","resources","app","meta","manifests","app_version.txt") + with open(APP_VERSION_FILE,"w") as f: + f.seek(0) + f.truncate() + f.write(APP_VERSION) + +if __name__ == "__main__": + write_appversion() diff --git a/source/classes/diags.py b/source/classes/diags.py index 3e2c4121..3919a382 100644 --- a/source/classes/diags.py +++ b/source/classes/diags.py @@ -1,16 +1,24 @@ import platform, sys, os, subprocess -import pkg_resources -from datetime import datetime +try: + import pkg_resources +except ModuleNotFoundError as e: + pass +import datetime + +from Main import __version__ +DR_VERSION = __version__ + +PROJECT_NAME = "ALttP Door Randomizer" def diagpad(str): - return str.ljust(len("ALttP Door Randomizer Version") + 5,'.') + return str.ljust(len(f"{PROJECT_NAME} Version") + 5,'.') -def output(APP_VERSION): +def output(): lines = [ - "ALttP Door Randomizer Diagnostics", + f"{PROJECT_NAME} Diagnostics", "=================================", - diagpad("UTC Time") + str(datetime.utcnow())[:19], - diagpad("ALttP Door Randomizer Version") + APP_VERSION, + diagpad("UTC Time") + str(datetime.datetime.now(datetime.UTC))[:19], + diagpad(f"{PROJECT_NAME} Version") + DR_VERSION, diagpad("Python Version") + platform.python_version() ] lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release())) @@ -35,6 +43,7 @@ def output(APP_VERSION): pkg = pkg.split("==") lines.append(diagpad(pkg[0]) + pkg[1]) ''' + installed_packages = [] 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: diff --git a/source/gui/randomize/generation.py b/source/gui/randomize/generation.py index c66eceb6..868402e4 100644 --- a/source/gui/randomize/generation.py +++ b/source/gui/randomize/generation.py @@ -154,7 +154,7 @@ def generation_page(parent,settings): diag.geometry(str(dims["window"]["width"]) + 'x' + str(dims["window"]["height"])) text = Text(diag, width=dims["textarea.characters"]["width"], height=dims["textarea.characters"]["height"]) text.pack() - text.insert(INSERT,"\n".join(diagnostics.output(__version__))) + text.insert(INSERT,"\n".join(diagnostics.output())) # dialog button self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"], text='Run Diagnostics', command=partial(diags)) diff --git a/source/meta/build-dr.py b/source/meta/build-dr.py deleted file mode 100644 index a83c9d56..00000000 --- a/source/meta/build-dr.py +++ /dev/null @@ -1,27 +0,0 @@ -import subprocess -import os -import shutil -import sys - -# Spec file -SPEC_FILE = os.path.join(".", "source", "DungeonRandomizer.spec") - -# Destination is current dir -DEST_DIRECTORY = '.' - -# Check for UPX -if os.path.isdir("upx"): - upx_string = "--upx-dir=upx" -else: - upx_string = "" - -if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.find("osx"): - shutil.rmtree("build") - -# Run pyinstaller for DungeonRandomizer -subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", - upx_string, - "-y ", - f"--distpath {DEST_DIRECTORY} ", - ]), - shell=True) diff --git a/source/meta/build-gui.py b/source/meta/build-gui.py deleted file mode 100644 index 4986df67..00000000 --- a/source/meta/build-gui.py +++ /dev/null @@ -1,27 +0,0 @@ -import subprocess -import os -import shutil -import sys - -# Spec file -SPEC_FILE = os.path.join(".", "source", "Gui.spec") - -# Destination is current dir -DEST_DIRECTORY = '.' - -# Check for UPX -if os.path.isdir("upx"): - upx_string = "--upx-dir=upx" -else: - upx_string = "" - -if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform.find("osx"): - shutil.rmtree("build") - -# Run pyinstaller for Gui -subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", - upx_string, - "-y ", - f"--distpath {DEST_DIRECTORY} ", - ]), - shell=True) diff --git a/source/meta/build.py b/source/meta/build.py new file mode 100644 index 00000000..4cc29436 --- /dev/null +++ b/source/meta/build.py @@ -0,0 +1,155 @@ +''' +Build Entrypoints +''' +import json +import platform +import os # for checking for dirs +import re +from json.decoder import JSONDecodeError +from subprocess import Popen, PIPE, STDOUT, CalledProcessError + +DEST_DIRECTORY = "." + +# UPX greatly reduces the filesize. You can get this utility from https://upx.github.io/ +# just place it in a subdirectory named "upx" and this script will find it +UPX_DIR = "upx" +if os.path.isdir(os.path.join(".", UPX_DIR)): + upx_string = f"--upx-dir={UPX_DIR}" +else: + upx_string = "" +GO = True +DIFF_DLLS = False + +# set a global var for Actions to try to read +def set_output(name, value): + with open(os.environ['GITHUB_OUTPUT'], 'a') as fh: + print(f'{name}={value}', file=fh) + +# build the thing +def run_build(slug): + global GO + global DIFF_DLLS + + print(f"Building '{slug}' via Python {platform.python_version()}") + + # get template, mod to do the thing + specTemplateFile = open(os.path.join(".","source","Template.spec")) + specTemplate = specTemplateFile.read() + specTemplateFile.close() + with(open(os.path.join(".","source",f"{slug}.spec"), "w")) as specFile: + print(f"Writing '{slug}' PyInstaller spec file") + thisTemplate = specTemplate.replace("", slug) + specFile.write(thisTemplate) + + PYINST_EXECUTABLE = "pyinstaller" + args = [ + os.path.join("source", f"{slug}.spec").replace(os.sep, os.sep * 2), + upx_string, + "-y", + f"--distpath={DEST_DIRECTORY}" + ] + errs = [] + strs = [] + print("PyInstaller args: %s" % " ".join(args)) + cmd = [ + PYINST_EXECUTABLE, + *args + ] + + ret = { + "stdout": [], + "stderr": [] + } + + with Popen(cmd, stdout=PIPE, stderr=STDOUT, bufsize=1, universal_newlines=True) as p: + for line in p.stdout: + ret["stdout"].append(line) + print(line, end='') + # if p.stderr: + # for line in p.stderr: + # ret["stderr"].append(line) + # print(line, end='') + # if p.returncode != 0: + # raise CalledProcessError(p.returncode, p.args) + + # check stdout & stderr + for key in ["stdout","stderr"]: + if len(ret[key]) > 0: + for line in ret[key]: + # UPX can't compress this file + if "NotCompressibleException" in line.strip(): + print(line) + errs.append(line.strip()) + # print UPX messages + if "UPX" in line: + print(line) + # try to get DLL filename + elif "NotCompressibleException" in line.strip(): + matches = re.search(r'api-ms-win-(?:[^-]*)-([^-]*)', line.strip()) + if matches: + strAdd = matches.group(1) + strs.append(strAdd) + errs.append(line.strip()) + # print collected errors + if len(errs) > 0: + print("=" * 10) + print("| ERRORS |") + print("=" * 10) + print("\n".join(errs)) + else: + GO = False + + # if we identified DLLs to ignore + if len(strs) > 0: + # read DLLs manifest that we've already got saved + with open(os.path.join(".","resources","app","meta","manifests","excluded_dlls.json"), "w+", encoding="utf-8") as dllsManifest: + oldDLLs = [] + try: + oldDLLs = json.load(dllsManifest) + except JSONDecodeError as e: + oldDLLs = [] + # raise ValueError("Windows DLLs manifest malformed!") + + # bucket for new list + newDLLs = sorted(list(set(oldDLLs))) + + # items to add + addDLLs = sorted(list(set(strs))) + + # add items + newDLLs += addDLLs + newDLLs = sorted(list(set(newDLLs))) + + # if the lists differ, we've gotta update the included list + diffDLLs = newDLLs != oldDLLs + + if diffDLLs: + DIFF_DLLS = True + dllsManifest.seek(0) + dllsManifest.truncate() + dllsManifest.write(json.dumps(sorted(newDLLs), indent=2)) + + print(f"Old DLLs: {json.dumps(sorted(oldDLLs))}") + print(f"Add DLLs: {json.dumps(sorted(addDLLs))}") + print(f"New DLLs: {json.dumps(sorted(newDLLs))}") + print(f"Diff DLLs: {DIFF_DLLS}") + print("") + +def go_build(slug): + slug = slug or "" + if slug != "": + GO = True + while GO: + run_build(slug) + GO = False + +if __name__ == "__main__": + binary_slugs = [] + #TODO: Make sure we've got the proper binaries that we need + with open(os.path.join(".","resources","app","meta","manifests","binaries.json")) as binariesFile: + binary_slugs = json.load(binariesFile) + for file_slug in binary_slugs: + go_build(file_slug) + if DIFF_DLLS: + print("🔴Had to update Error DLLs list!") + exit(1) diff --git a/source/meta/run_diags.py b/source/meta/run_diags.py new file mode 100644 index 00000000..0424674a --- /dev/null +++ b/source/meta/run_diags.py @@ -0,0 +1,10 @@ +from source.classes import diags as diags + +global VERBOSE +VERBOSE = True + +if __name__ == "__main__": + if VERBOSE: + print("DIAGNOSTICS") + print('.' * 70) + print("\n".join(diags.output())) diff --git a/test/MysteryTestSuite.py b/test/MysteryTestSuite.py index ea155dd8..e14baa61 100644 --- a/test/MysteryTestSuite.py +++ b/test/MysteryTestSuite.py @@ -1,3 +1,4 @@ +import os import subprocess import sys import multiprocessing @@ -8,6 +9,16 @@ from collections import OrderedDict cpu_threads = multiprocessing.cpu_count() py_version = f"{sys.version_info.major}.{sys.version_info.minor}" +PYLINE = "python" +PIPLINE_PATH = os.path.join(".","resources","user","meta","manifests","pipline.txt") +if os.path.isfile(PIPLINE_PATH): + with open(PIPLINE_PATH) as pipline_file: + PYLINE = pipline_file.read().replace("-m pip","").strip() + +results = { + "errors": [], + "success": [] +} def main(args=None): successes = [] @@ -25,7 +36,7 @@ def main(args=None): def test(testname: str, command: str): tests[testname] = [command] - basecommand = f"python3.8 Mystery.py --suppress_rom --suppress_meta" + basecommand = f"{PYLINE} Mystery.py --suppress_rom --suppress_meta" def gen_seed(): taskcommand = basecommand + " " + command @@ -98,6 +109,10 @@ if __name__ == "__main__": cpu_threads = args.cpu_threads + LOGPATH = os.path.join(".","logs") + if not os.path.isdir(LOGPATH): + os.makedirs(LOGPATH) + for dr in [['mystery', args.count if args.count else 1, 1]]: for tense in range(1, dr[2] + 1): @@ -112,13 +127,36 @@ if __name__ == "__main__": print() if errors: - with open(f"{dr[0]}{(f'-{tense}' if dr[0] in ['basic', 'crossed'] else '')}-errors.txt", 'w') as stream: + errors_filename = f"{dr[0]}" + if dr[0] in ["basic","crossed"]: + errors_filename += f"-{tense}" + errors_filename += "-errors.txt" + with open( + os.path.join( + LOGPATH, + errors_filename + ), + 'w' + ) as stream: for error in errors: stream.write(error[0] + "\n") stream.write(error[1] + "\n") stream.write(error[2] + "\n\n") + error[2] = error[2].split("\n") + results["errors"].append(error) - with open("success.txt", "w") as stream: + with open(os.path.join(LOGPATH, "mystery-success.txt"), "w") as stream: stream.write(str.join("\n", successes)) + results["success"] = successes - input("Press enter to continue") + num_errors = len(results["errors"]) + num_success = len(results["success"]) + num_total = num_errors + num_success + + print(f"Errors: {num_errors}/{num_total}") + print(f"Success: {num_success}/{num_total}") + # print(results) + + if (num_errors/num_total) > (num_success/num_total): + # exit(1) + pass diff --git a/test/NewTestSuite.py b/test/NewTestSuite.py index 8e7b9e1c..0176c156 100644 --- a/test/NewTestSuite.py +++ b/test/NewTestSuite.py @@ -10,6 +10,16 @@ from collections import OrderedDict cpu_threads = multiprocessing.cpu_count() py_version = f"{sys.version_info.major}.{sys.version_info.minor}" +PYLINE = "python" +PIPLINE_PATH = os.path.join(".","resources","user","meta","manifests","pipline.txt") +if os.path.isfile(PIPLINE_PATH): + with open(PIPLINE_PATH) as pipline_file: + PYLINE = pipline_file.read().replace("-m pip","").strip() + +results = { + "errors": [], + "success": [] +} def main(args=None): successes = [] @@ -28,7 +38,7 @@ def main(args=None): def test(test_name: str, command: str, test_file: str): tests[test_name] = [command] - base_command = f"python3 DungeonRandomizer.py --suppress_rom --suppress_spoiler" + base_command = f"{PYLINE} DungeonRandomizer.py --suppress_rom --jsonout --spoiler none" def gen_seed(): task_command = base_command + " " + command @@ -102,7 +112,7 @@ if __name__ == "__main__": test_suites = {} # not sure if it supports subdirectories properly yet - for root, dirnames, filenames in os.walk('test/suite'): + for root, dirnames, filenames in os.walk(os.path.join("test","suite")): test_suites[root] = fnmatch.filter(filenames, '*.yaml') args = argparse.Namespace() @@ -113,14 +123,30 @@ if __name__ == "__main__": successes += s print() + LOGPATH = os.path.join(".","logs") + if not os.path.isdir(LOGPATH): + os.makedirs(LOGPATH) + if errors: - with open(f"new-test-suite-errors.txt", 'w') as stream: + with open(os.path.join(LOGPATH, "new-test-suite-errors.txt"), 'w') as stream: for error in errors: stream.write(error[0] + "\n") stream.write(error[1] + "\n") stream.write(error[2] + "\n\n") + error[2] = error[2].split("\n") + results["errors"].append(error) - with open("new-test-suite-success.txt", "w") as stream: + with open(os.path.join(LOGPATH, "new-test-suite-success.txt"), 'w') as stream: stream.write(str.join("\n", successes)) + results["success"] = successes - input("Press enter to continue") + num_errors = len(results["errors"]) + num_success = len(results["success"]) + num_total = num_errors + num_success + + print(f"Errors: {num_errors}/{num_total}") + print(f"Success: {num_success}/{num_total}") + # print(results) + + if (num_errors/num_total) > (num_success/num_total): + exit(1) From 8240118d7e0948b226377a42b598861a5573b0fd Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 30 May 2024 14:25:04 -0600 Subject: [PATCH 08/28] build: ci changes --- .github/workflows/release-complete.yml | 2 +- .github/workflows/release-create.yml | 4 ---- resources/ci/common/common.py | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-complete.yml b/.github/workflows/release-complete.yml index a193e0ec..80f7a4f3 100644 --- a/.github/workflows/release-complete.yml +++ b/.github/workflows/release-complete.yml @@ -43,5 +43,5 @@ jobs: FINE_PAT: ${{ secrets.ALTTPER_TAGGER }} with: repository: ${{ github.repository_owner }}/z3randomizer - ref-name: heads/OWMain + ref-name: heads/DRUnstable github-tag: ${{ github.event.release.tag_name }} diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml index 3aacc0b8..aab283b2 100644 --- a/.github/workflows/release-create.yml +++ b/.github/workflows/release-create.yml @@ -43,7 +43,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -102,7 +101,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -178,7 +176,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] @@ -220,7 +217,6 @@ jobs: os-name: [ # ubuntu-latest, # ubuntu-22.04 ubuntu-22.04, - ubuntu-20.04, macos-latest, # macos-12 windows-latest # windows-2022 ] diff --git a/resources/ci/common/common.py b/resources/ci/common/common.py index 5329c768..148c7043 100644 --- a/resources/ci/common/common.py +++ b/resources/ci/common/common.py @@ -19,7 +19,7 @@ global FILESIZE_CHECK # windows: 2022, 2019 # macos: 14, 13, 12, 11 DEFAULT_EVENT = "event" -DEFAULT_REPO_SLUG = "miketrethewey/ALttPDoorRandomizer" +DEFAULT_REPO_SLUG = "aerinon/ALttPDoorRandomizer" FILENAME_CHECKS = [ "DungeonRandomizer", "Gui", From 97178787a9b383205c394ed9d86cb2f057599772 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 30 May 2024 14:47:52 -0600 Subject: [PATCH 09/28] build: ci changes --- .github/workflows/release-create.yml | 30 ++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml index aab283b2..ebf00702 100644 --- a/.github/workflows/release-create.yml +++ b/.github/workflows/release-create.yml @@ -354,12 +354,10 @@ jobs: echo "MacOS Asset: ${{ steps.identify-macos-asset.outputs.asset_macos }}" echo "Windows Asset: ${{ steps.identify-windows-asset.outputs.asset_windows }}" - # create a release (MASTER) - #TODO: Make sure we updated RELEASENOTES.md - #TODO: Make sure we're firing on the proper branches - # if: contains(github.ref, 'master') # branch or tag name + # create a release (DoorDevUnstable) + # if: contains(github.ref, 'DoorDevUnstable') # branch or tag name # if: contains(github.event.head_commit.message, 'Version bump') # commit message - - name: 📀->🚀Create a Release (MASTER) + - name: 📀->🚀Create a Release (DoorDevUnstable) id: create_release uses: actions/create-release@v1.1.4 env: @@ -369,11 +367,10 @@ jobs: release_name: ${{ steps.debug_info.outputs.release_name }} body_path: RELEASENOTES.md # draft: true - if: contains(github.ref, 'master') + if: contains(github.ref, 'DoorDevUnstable') - # upload linux archive asset (MASTER) - #TODO: Make sure we're firing on the proper branches - - name: 🔼Upload Linux Archive Asset (MASTER) + # upload linux archive asset (DoorDevUnstable) + - name: 🔼Upload Linux Archive Asset (DoorDevUnstable) id: upload-linux-asset uses: actions/upload-release-asset@v1.0.2 env: @@ -383,11 +380,10 @@ jobs: asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/linux/${{ steps.identify-linux-asset.outputs.asset_linux }} asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-linux-focal.tar.gz asset_content_type: application/gzip - if: contains(github.ref, 'master') + if: contains(github.ref, 'DoorDevUnstable') - # upload macos archive asset (MASTER) - #TODO: Make sure we're firing on the proper branches - - name: 🔼Upload MacOS Archive Asset (MASTER) + # upload macos archive asset (DoorDevUnstable) + - name: 🔼Upload MacOS Archive Asset (DoorDevUnstable) id: upload-macos-asset uses: actions/upload-release-asset@v1.0.2 env: @@ -397,11 +393,11 @@ jobs: asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/macos/${{ steps.identify-macos-asset.outputs.asset_macos }} asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-osx.tar.gz asset_content_type: application/gzip - if: contains(github.ref, 'master') + if: contains(github.ref, 'DoorDevUnstable') - # upload windows archive asset (MASTER) + # upload windows archive asset (DoorDevUnstable) #TODO: Make sure we're firing on the proper branches - - name: 🔼Upload Windows Archive Asset (MASTER) + - name: 🔼Upload Windows Archive Asset (DoorDevUnstable) id: upload-windows-asset uses: actions/upload-release-asset@v1.0.2 env: @@ -411,4 +407,4 @@ jobs: asset_path: ${{ steps.parentDir.outputs.parentDir }}/deploy/windows/${{ steps.identify-windows-asset.outputs.asset_windows }} asset_name: ${{ steps.debug_info.outputs.asset_prefix }}-windows.zip asset_content_type: application/zip - if: contains(github.ref, 'master') + if: contains(github.ref, 'DoorDevUnstable') From 4315b999081ede31e1ff5b2fe94236f91e919430 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 6 Jun 2024 15:00:39 -0600 Subject: [PATCH 10/28] fix: Swapped and Simple ER generation errors --- source/overworld/EntranceShuffle2.py | 58 +++++++++++++++------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 9dfb7de9..f6f38ae6 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -17,6 +17,7 @@ class EntrancePool(object): self.swapped = False self.default_map = {} self.one_way_map = {} + self.combine_map = {} self.skull_handled = False self.links_on_mountain = False self.decoupled_entrances = [] @@ -77,6 +78,7 @@ def link_entrances_new(world, player): default_map['Big Bomb Shop'] = 'Links House Exit' avail_pool.default_map = default_map avail_pool.one_way_map = one_way_map + avail_pool.combine_map = {**default_map, **one_way_map} # setup mandatory connections for exit_name, region_name in mandatory_connections: @@ -261,7 +263,8 @@ def do_main_shuffle(entrances, exits, avail, mode_def): # cross world mandantory entrance_list = list(entrances) if avail.swapped: - forbidden = [e for e in Forbidden_Swap_Entrances if e in entrance_list] + ban_list = Forbidden_Swap_Entrances_Inv if avail.inverted else Forbidden_Swap_Entrances + forbidden = [e for e in ban_list if e in entrance_list] entrance_list = [e for e in entrance_list if e not in forbidden] must_exit, multi_exit_caves = figure_out_must_exits_cross_world(entrances, exits, avail) do_mandatory_connections(avail, entrance_list, multi_exit_caves, must_exit) @@ -469,20 +472,22 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe if not cross_world: if 'Sanctuary Grave' in holes_to_shuffle: hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) - is_hc_in_dw = avail.world.mode[avail.player] == 'inverted' + is_hc_in_opp_world = avail.inverted if hc.connected_region: - is_hc_in_dw = hc.connected_region.type == RegionType.DarkWorld + is_hc_in_opp_world = hc.connected_region.type == RegionType.DarkWorld + start_world_entrances = DW_Entrances if avail.inverted else LW_Entrances + opp_world_entrances = LW_Entrances if avail.inverted else DW_Entrances chosen_entrance = None - if is_hc_in_dw: + if is_hc_in_opp_world: if avail.swapped: - chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances and e[0] != 'Sanctuary') + chosen_entrance = next(e for e in hole_entrances if e[0] in opp_world_entrances and e[0] != 'Sanctuary') if not chosen_entrance: - chosen_entrance = next(e for e in hole_entrances if e[0] in DW_Entrances) + chosen_entrance = next((e for e in hole_entrances if e[0] in opp_world_entrances), None) if not chosen_entrance: if avail.swapped: - chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances and e[0] != 'Sanctuary') + chosen_entrance = next(e for e in hole_entrances if e[0] in start_world_entrances and e[0] != 'Sanctuary') if not chosen_entrance: - chosen_entrance = next(e for e in hole_entrances if e[0] in LW_Entrances) + chosen_entrance = next(e for e in hole_entrances if e[0] in start_world_entrances) if chosen_entrance: connect_hole_via_interior(chosen_entrance, 'Sanctuary Exit', hole_entrances, hole_targets, entrances, exits, avail) @@ -831,7 +836,7 @@ def do_cross_world_connectors(entrances, caves, avail): avail.decoupled_entrances.remove(choice) else: if avail.swapped and len(entrances) > 1: - chosen_entrance = next(e for e in entrances if combine_map[e] != ext) + chosen_entrance = next(e for e in entrances if avail.combine_map[e] != ext) entrances.remove(chosen_entrance) else: chosen_entrance = entrances.pop() @@ -1098,7 +1103,7 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): if entrance not in entrances: entrances.append(entrance) if avail.swapped: - swap_forbidden = [e for e in entrances if combine_map[e] in must_exit] + swap_forbidden = [e for e in entrances if avail.combine_map[e] in must_exit] for e in swap_forbidden: entrances.remove(e) entrances.sort() # sort these for consistency @@ -1135,7 +1140,7 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): for candidate in cave_options: if not isinstance(candidate, str) and len(candidate) > 1 and (candidate in used_caves or len(candidate) < len(entrances) - required_entrances): - if not avail.swapped or (combine_map[exit] not in candidate and not any(e for e in must_exit if combine_map[e] in candidate)): #maybe someday allow these, but we need to disallow mutual locks in Swapped + if not avail.swapped or (avail.combine_map[exit] not in candidate and not any(e for e in must_exit if avail.combine_map[e] in candidate)): #maybe someday allow these, but we need to disallow mutual locks in Swapped candidates.append(candidate) cave = random.choice(candidates) @@ -1164,10 +1169,10 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): if len(cave) == 2: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)] and e not in must_exit - and (not avail.swapped or rnd_cave[0] != combine_map[e])) + and (not avail.swapped or rnd_cave[0] != avail.combine_map[e])) entrances.remove(entrance) connect_two_way(entrance, rnd_cave[0], avail) - if avail.swapped and combine_map[entrance] != rnd_cave[0]: + if avail.swapped and avail.combine_map[entrance] != rnd_cave[0]: swap_ent, _ = connect_cave_swap(entrance, rnd_cave[0], cave) entrances.remove(swap_ent) if cave in used_caves: @@ -1184,11 +1189,11 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): cave_entrances.append(entrance) else: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit - and (not avail.swapped or cave_exit != combine_map[e])) + and (not avail.swapped or cave_exit != avail.combine_map[e])) cave_entrances.append(entrance) entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) - if avail.swapped and combine_map[entrance] != cave_exit: + if avail.swapped and avail.combine_map[entrance] != cave_exit: swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) entrances.remove(swap_ent) if entrance not in invalid_connections: @@ -1215,11 +1220,11 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): continue else: entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)] - and (not avail.swapped or cave_exit != combine_map[e])) + and (not avail.swapped or cave_exit != avail.combine_map[e])) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) - if avail.swapped and combine_map[entrance] != cave_exit: + if avail.swapped and avail.combine_map[entrance] != cave_exit: swap_ent, _ = connect_cave_swap(entrance, cave_exit, cave) entrances.remove(swap_ent) cave_options.remove(cave) @@ -1346,11 +1351,11 @@ def connect_swapped(entrancelist, targetlist, avail, two_way=False): random.shuffle(entrancelist) sorted_targets = list() for ent in entrancelist: - if ent in combine_map: - if combine_map[ent] not in targetlist: - logging.getLogger('').error(f'{combine_map[ent]} not in target list, cannot swap entrance') - raise Exception(f'{combine_map[ent]} not in target list, cannot swap entrance') - sorted_targets.append(combine_map[ent]) + if ent in avail.combine_map: + if avail.combine_map[ent] not in targetlist: + logging.getLogger('').error(f'{avail.combine_map[ent]} not in target list, cannot swap entrance') + raise Exception(f'{avail.combine_map[ent]} not in target list, cannot swap entrance') + sorted_targets.append(avail.combine_map[ent]) if len(sorted_targets): targetlist = list(sorted_targets) else: @@ -1371,12 +1376,12 @@ def connect_swapped(entrancelist, targetlist, avail, two_way=False): def connect_swap(entrance, exit, avail): - swap_exit = combine_map[entrance] + swap_exit = avail.combine_map[entrance] if swap_exit != exit: - swap_entrance = next(e for e, x in combine_map.items() if x == exit) + swap_entrance = next(e for e, x in avail.combine_map.items() if x == exit) if swap_entrance in ['Pyramid Entrance', 'Pyramid Hole'] and avail.inverted: swap_entrance = 'Inverted ' + swap_entrance - if entrance in entrance_map: + if swap_exit in entrance_map.values(): connect_two_way(swap_entrance, swap_exit, avail) else: connect_entrance(swap_entrance, swap_exit, avail) @@ -2063,8 +2068,6 @@ single_entrance_map = { 'Blinds Hideout': 'Blinds Hideout', 'Waterfall of Wishing': 'Waterfall of Wishing' } -combine_map = {**entrance_map, **single_entrance_map, **drop_map} - default_dw = { 'Thieves Town Exit', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)', 'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit', @@ -2320,6 +2323,7 @@ Inverted_Bomb_Shop_Options = [ Forbidden_Swap_Entrances = {'Old Man Cave (East)', 'Blacksmiths Hut', 'Big Bomb Shop'} +Forbidden_Swap_Entrances_Inv = {'Dark Death Mountain Fairy', 'Blacksmiths Hut', 'Links House'} # these are connections that cannot be shuffled and always exist. # They link together separate parts of the world we need to divide into regions From 0d536a593bfc66cb5211f0fb06af5699fc8e7d8a Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 07:35:11 -0600 Subject: [PATCH 11/28] change: python cmd not version specific --- TestSuite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestSuite.py b/TestSuite.py index d8a1f3e1..7014623f 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -29,7 +29,7 @@ def main(args=None): ['Std ', ' --mode standard'], ['Inv ', ' --mode inverted']]: - basecommand = f"python3.8 DungeonRandomizer.py --door_shuffle {args.dr} --intensity {args.tense} --suppress_rom --suppress_spoiler" + basecommand = f"python DungeonRandomizer.py --door_shuffle {args.dr} --intensity {args.tense} --suppress_rom --suppress_spoiler" def gen_seed(): taskcommand = basecommand + " " + command + mode[1] From 7fe544861f08b0de78c38ed706c56311dffbfdde Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 07:35:59 -0600 Subject: [PATCH 12/28] fix: ensure crystals reachable wasn't properly accounting for lobbies with switches --- DungeonGenerator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5f34fbf2..2cae4254 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1848,10 +1848,8 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s sector.equations = calc_sector_equations(sector) if sector.is_entrance_sector() and not sector.destination_entrance: need_switch = True - for region in sector.get_start_regions(): - if region.crystal_switch: - need_switch = False - break + if sector.c_switch: # this relies on the fact that Mire Fishbone SE cannot be a portal + need_switch = False any_benefit = False for eq in sector.equations: if len(eq.benefit) > 0: @@ -4033,7 +4031,7 @@ def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None): eq = DoorEquation(door) eq.benefit[hook_from_door(door)].append(door) eq.required = True - eq.c_switch = door.crystal == CrystalBarrier.Either + eq.c_switch = sector.c_switch # Big change - not true for mire fishbone, need to verify for others # exceptions for long entrances ??? # if door.name in ['PoD Dark Alley']: eq.entrance_flag = True From 5740a03c258a447cfa514da59739b4b56665bec7 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 10:55:04 -0600 Subject: [PATCH 13/28] fix: testsuite with new spoiler setting --- TestSuite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestSuite.py b/TestSuite.py index 7014623f..60d9d37b 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -29,7 +29,7 @@ def main(args=None): ['Std ', ' --mode standard'], ['Inv ', ' --mode inverted']]: - basecommand = f"python DungeonRandomizer.py --door_shuffle {args.dr} --intensity {args.tense} --suppress_rom --suppress_spoiler" + basecommand = f"python DungeonRandomizer.py --door_shuffle {args.dr} --intensity {args.tense} --suppress_rom --spoiler none" def gen_seed(): taskcommand = basecommand + " " + command + mode[1] From 6d129edc3c61b6c7a5f7b5e64eb2137cffd6c7c1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 12:25:07 -0600 Subject: [PATCH 14/28] fix: inverted ER issues --- source/overworld/EntranceShuffle2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index f6f38ae6..0631545a 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -394,8 +394,6 @@ def do_old_man_cave_exit(entrances, exits, avail, cross_world): if avail.inverted and cross_world: om_cave_options = Inverted_Old_Man_Entrances + Old_Man_Entrances om_cave_options = [x for x in om_cave_options if x in entrances] - if avail.swapped: - om_cave_options = [e for e in om_cave_options if e not in Forbidden_Swap_Entrances] om_cave_choice = random.choice(om_cave_options) if not avail.coupled: connect_exit('Old Man Cave Exit (East)', om_cave_choice, avail) @@ -403,7 +401,8 @@ def do_old_man_cave_exit(entrances, exits, avail, cross_world): else: connect_two_way(om_cave_choice, 'Old Man Cave Exit (East)', avail) entrances.remove(om_cave_choice) - if avail.swapped and om_cave_choice != 'Old Man Cave (East)': + default_entrance = 'Dark Death Mountain Fairy' if avail.inverted else 'Old Man Cave (East)' + if avail.swapped and om_cave_choice != default_entrance: swap_ent, swap_ext = connect_swap(om_cave_choice, 'Old Man Cave Exit (East)', avail) entrances.remove(swap_ent) exits.remove(swap_ext) @@ -474,7 +473,8 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe hc = avail.world.get_entrance('Hyrule Castle Exit (South)', avail.player) is_hc_in_opp_world = avail.inverted if hc.connected_region: - is_hc_in_opp_world = hc.connected_region.type == RegionType.DarkWorld + opp_world = RegionType.LightWorld if avail.inverted else RegionType.DarkWorld + is_hc_in_opp_world = hc.connected_region.type == opp_world start_world_entrances = DW_Entrances if avail.inverted else LW_Entrances opp_world_entrances = LW_Entrances if avail.inverted else DW_Entrances chosen_entrance = None From 119c863f064032467ffa42be721ebd4059f1136f Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 14:05:51 -0600 Subject: [PATCH 15/28] fix: insanity gen failure --- source/overworld/EntranceShuffle2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 0631545a..1e255b64 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -751,6 +751,7 @@ def figure_out_must_exits_cross_world(entrances, exits, avail): if not avail.skull_handled: skull_connector = [x for x in ['Skull Woods Second Section Exit (West)', 'Skull Woods Second Section Exit (East)'] if x in exits] multi_exit_caves.append(skull_connector) + remove_from_list(exits, skull_connector) must_exit_lw = (Inverted_LW_Must_Exit if avail.inverted else LW_Must_Exit).copy() must_exit_dw = (Inverted_DW_Must_Exit if avail.inverted else DW_Must_Exit).copy() From 9898b5bcb613171119d8e6878abfa52651cb6163 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 15:08:30 -0600 Subject: [PATCH 16/28] doc: version bump and note --- Main.py | 2 +- RELEASENOTES.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index f59637d0..a533cb7b 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.2' +version_number = '1.4.3' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8593d406..04aea2df 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -182,6 +182,8 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes +* 1.4.3 + * Generation: Fixed several generation problems with ER and intensity 3 * 1.4.2 * New ER Options: * [Skull Woods shuffle options](#skull-woods-shuffle) From 336a9379b60f63b3062ac2a9873e0fbcbd65dd37 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 7 Jun 2024 15:48:43 -0600 Subject: [PATCH 17/28] change: renamed "default" key logic algorithm to "dangerous" --- BaseClasses.py | 6 +- CHANGELOG.md | 354 ++++++++++++ README.md | 68 ++- RELEASENOTES.md | 533 +----------------- Rules.py | 2 +- mystery_example.yml | 4 +- mystery_testsuite.yml | 2 +- resources/app/cli/args.json | 2 +- resources/app/cli/lang/en.json | 2 +- resources/app/gui/lang/en.json | 2 +- .../app/gui/randomize/dungeon/widgets.json | 6 +- test/suite/default_key_logic.yaml | 2 +- 12 files changed, 435 insertions(+), 548 deletions(-) create mode 100644 CHANGELOG.md diff --git a/BaseClasses.py b/BaseClasses.py index 868eac4e..0d63917c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -575,7 +575,7 @@ class CollectionState(object): queue = deque(old_state.blocked_connections[player].items()) old_state.traverse_world(queue, rrp, bc, player) - if old_state.world.key_logic_algorithm[player] == 'default': + if old_state.world.key_logic_algorithm[player] == 'dangerous': unresolved_events = [x for y in old_state.reachable_regions[player] for x in y.locations if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) and x not in old_state.locations_checked and x.can_reach(old_state)] @@ -603,7 +603,7 @@ class CollectionState(object): queue = deque(self.blocked_connections[player].items()) self.traverse_world(queue, rrp, bc, player) - if self.world.key_logic_algorithm[player] == 'default': + if self.world.key_logic_algorithm[player] == 'dangerous': unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) and x not in self.locations_checked and x.can_reach(self)] @@ -3123,7 +3123,7 @@ bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} -key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} +key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} # byte 13: SSDD ???? (skullwoods, linked_drops, 4 free bytes) skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a306a6e6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,354 @@ +# Patch Notes + +Changelog archive + +* 1.4.2 + * New ER Options: + * [Skull Woods shuffle options](#skull-woods-shuffle) + * [New option](#linked-drops-override) to override linked drop down behavior + * Customizer: You can now start with a "RandomWeapon" in the start inventory section + * Customizer: You may now start with "Big Magic" or "Small Magic" items + * Customizer: Suppress warning for missing items if they are in start inventory + * MultiClient: change default port to 23074 for newer SNI versions + * Generation: Fixed typo causing ER gen failure with Bomb Shop at Graveyard Ledge +* 1.4.1.12u + * New Entrance Shuffle Algorithm no longer experimental + * Back of Tavern Shuffle now on by default + * Enemizer: Wallmasters banned from tiles where spiral staircases are. (Softlock issue) + * Packaged build of unstable now available + * Customizer: New PreferredLocationGroup for putting a set of items in a set of locations. See customizer docs. + * Customizer: Fixed an issue with starting with `Ocarina` and flute_mode is active + * Spoiler: Some reformatting. Crystal req. for GT/Ganon moved to requirements section so randomized requirements don't show up in the meta section + * Algorithm: Major_Only. Supports up to 16 extra locations (the visible heart pieces) for when major item count exceeds major location count. Examples: Triforce Hunt, Trinity (Triforce on Ped), Bombbag shuffle + * Fix: HC Big Key drop doesn't count on Basic Doors + * Fix: Small Key for this dungeon in Hera Basement doesn't count twice for the key counter + * Fix: All cross-dungeon modes with restrict boss items should require map/compass for the boss + * Fixed a small bug with traversal algorithm + * Enemizer: Enemy bans+ +* 1.4.1.11u + * New Feature: Several spoiler levels added: None, Settings-only, Semi, Full, Debug + * Semi includes only entrances, prizes, and medallions (potential new spoiler mode being worked on, definition may change) + * Entrance: Lite/Lean support enemy drop shuffle + * Standard: Re-added tutorial guard near large rock + * Enemizer + * Fixed the overwriting of bonk fairies + * Fixed broken graphics on hyrule castle + * Enemy bans + * Customizer: Fixed bug with customizing prize packs +* 1.4.1.10u + * Vanilla key logic: Fix for vanilla layout Misery Mire which allows more complex key logic. Locations blocked by crystal switch access are only locked by 2 keys thanks to that being the minimum in Mire to reach one of two crystal switches. + * Autotracking: Fix for chest turn counter with chest containing multiworld items (Thanks Hiimcody) + * Enemizer: Enemy bans + * Rom: Code prettification and fixing byte designations by Codemann + * Support added for BPS patches via jsonout setting (Thanks Veetorp!) +* 1.4.1.9u + * Enemy Drop Underworld: Changed enemy drop indicator to not require compass + * Experimental: Moved dark world bunny spawns out of experimental. (It is now always on) + * Fix: Red/Blue pendants were swapped for autotracking. (Thanks Muffins!) + * Fix: Red square sometimes wasn't blinking + * Updated tournament winners + * Enemizer: Enemy bans +* 1.4.1.8u + * HUD: New dungeon indicators based on common abbreviations + * OWG+HMG: EG is allowed to be armed + * Drop Shuffle: Fixed an issue with minor drops counting for the wrong dungeon + * Enemizer: Trinexx ice breath should be properly disabled if Trinexx is located outside of Turtle Rock + * Enemizer: Enemy bans +* 1.4.1.7u + * Some bugs around Triforce Pieces smoothed out + * Enemizer: No exception for mimics/eyegores in vanilla rooms if enemy logic is turned to off + * Enemizer: Various enemy bans +* 1.4.1.6u + * Difficulty: Fixed some issues around item caps not being respected + * Enemezier: Tutorial guards remove from South Kakariko + * Map: Pendant colors fixed + * Minor rom code cleanup + * Enemizer: Hovers added to problematic pool near pits. Some other bans +* 1.4.1.5u + * Major Fix: Problem with Ganon's Room sprites + * HMG: Remove extra Swamp Smalls in the pool due to pottery settings + * Enemizer: Couple enemy bans +* 1.4.1.4u + * Logic: Fixed logic bugs surrounding dynammic doors missing logic from big keys and other door types + * Inverted: Castle warp should not appear after defeating Aga 1 + * Enemzier: Fixed a crash with cached sprites Zora/Swamola +* 1.4.1.3v + * Enemizer: Raven/Murderdactyls using the correct damage table + * Enemzier: Boss drops only center when boss shuffle is on +* 1.4.1.2v + * Expert/Hard Item Pool: Capacity fairy no longer gives out free crystal + * Vanilla door + Universal Keys: Generation fixed + * Boss Shuffle: Generation fixed (thanks Codemann for easy solution) + * Vanilla ER: No need for ability to check prizes on keysanity menu + * Swapped ER: Possible generation issue fixed (thanks Codemann) + * Enemizer: Roller ban + * Performance: Faster text boxes. Thanks Kan! +* 1.4.1.1v + * Logic: Moon pearl logic respects blocked doors +* 1.4.1.0v + * World Model Refactor: The overworld has been split up by screen, brings OR and DR a bit closer together in the model sense. A few OWG clips have been rewritten to fit into this new logic better. + * Logic: New logic for some bosses on ice + * Helmasaur on Ice: Bombs for mask, sword or arrows for 2nd phase + * Blind on Ice: Beam sword, Somaria, or Byrna plus magic extension for damage. Red shield or Byrna for protection. + * Kholdstare on Ice: Three options (after cracking the shell) + * Beam sword + * Fire Rod with 1.5 magic extensions + * Fire Rod & Bombos & any Sword & 1 Magic Extension + * Vitreous on Ice: Arrows and Bombs or a Beam Sword + * Trinexx on Ice: Boots always required for dodging. Damage options: + * Gold sword + * Tempered sword with magic extension + * Hammer or Master sword with 3 magic extensions (Rod spam for elemental heads, non-ideal weapon for last phase) + * Trinexx on Ice forbidden in doors seeds until we can model some health requirements. Low health Trinexx still isn't realistically feasible (bascially playing OHKO) + * Logic: Added silver arrows as Arrghus damage option when item functionality is not set to hard or expert + * Logic: Byrna not in logic for laser bridge when item functionality is set to hard or expert + * Enemizer Damage Rework: + * Shuffled: Actually shuffles the damage groups in the table instead of picking random numbers and reducing for mails from there. Enemies will still be assigned to a damage group randomly. + * There will always be at least one group which does no damage. The thief will always be in that group. Ganon always has his own group. + * Glitched modes: Aga 1 should be vulnerable in rain state for glitched modes + * Generation: Trinexx and Lanmolas room allowed as lobbies in intensity 3 (works with enemizer now) + * Enemy AI: Terrorpin AI code removed. May help with unusual enemy behavior? +* 1.4.0.1v + * Key logic: Vanilla key logic fixes. Statically set some HC logic and PoD front door + * Generation: Fix a broken tile pattern + * Inverted: Castle warp should not appear after defeating Aga 1 + * Murahdahla: Should not disappear after Aga 1. May fix other subtle issues. + * Shopsanity: Buying multiple of an item in the potion shop should no longer increase item count. +* 1.4.0.0v + * Initial support for HMG (Thanks Muffins!) + * Generation: fix for bunny walk logic taking up too much memory + * Key Logic: Partial is now the new default + * Enemizer: enemy bans +* 1.3.0.9v + * ER: New Swapped ER mode borrowed from OWR + * ER: fixed a generation error where TR chooses all "must-exits" + * Ganonhunt: playthrough no longer collects crystals + * Vanilla Fill: Uncle weapon is always a sword, medallions for Mire/TR will be vanilla + * Customizer: support shufflebosses/shuffleenemies as well as boss_shuffle/enemy_shuffle + * Enemizer: enemy bans +* 1.3.0.8v + * Enemizer: Red Mimics correctly banned from challenge rooms in appropriate logic setting + * No Logic Standard ER: Rain doors aren't blocked if no logic is enabled. + * Trinexx: attempt to fix early start + * MW Progression Balancing: Change to be percentage based instead of raw count. (80% threshold) + * Take anys: Good Bee cave chosen as take any should no longer prevent generation + * Money balancing: Fixed generation issue + * Enemizer: various enemy bans +* 1.3.0.7v + * Fix for Mimic Cave enemy drops + * Fix for Spectacle Rock Cave enemy drops (the mini-moldorms) + * Fix for multiworld lamps with incorrect graphics + * No longer shuffles fairy bonks (from trees) as part of Enemizer +* 1.3.0.6v + * Flute can't be activated in rain state (except glitched modes) (Thanks codemann!) + * Enemizer + * Arrghus at Lanmo 2 no longer prevents pot pickups + * Trinexx at Lanmo 2 requires the Cape go backwards to face him + * Lift-able Blocks require a sprite slot (should help reduce problems) + * Fixed logic issues: + * Self-locking key not allowed in Sanctuary in standard (typo fixed) + * More advanced bunny-walking logic in dungeons (multiple paths considered) + * ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used) + * MSU: GTBK song fix for DR (Thanks codemann!) + * District Algorithm: Fails if no available location outside chosen districts + * Various enemy bans + * More Gibos near kiki and Old Man + * Bumper/AntiFairy obstacles + * Damaging roller + * Statue + Pots don't mix + * Statues on Skull Big Key Chest tile + * Toppo in challenge rooms + * Misc others +* 1.3.0.5v + * Hud/Map Counter: Collecting a keys for this dungeon of a bonk torch no longer increments the counter twice and immediately updates the hud. + * Enemizer: Hera basement item counting twice fixed by banning wallmasters on the tile. + * Enemizer: Statues banned offscreen for pull switches + * Enemizer: Several sprite producing enemies have been limited on crowded tiles. Offenders: Hinox, Sluggula, Bomb Guard, Beamos, Gibo, Wall Cannons, Probe using Guards. Others do not spam as many projectiles. + * Enemizer: More enemy bans (mostly Wizzrobes near walls where they won't spawn, couple missed firebar spots) +* 1.3.0.4v + * Enemizer: The bunny beam near Lanmo 2 and the 4 fairies near Ice Armos are not shuffled anymore. This is due to how bosses shuffle works and since it cannot be guaranteed to work within the current system, they are vanilla. (Vitreous still overwrites the fairies and Arrghus only lets two spawn, etc.) + * Dropshuffle: Pokey 1 has been fixed to drop his item + * Mystery/Customizer: true/false and on/off in yaml files should behave the same. + * More enemy bans as have been reported +* 1.3.0.3v + * Faeries now part of the enemy shuffle pool. Take note, this will increase enemy drop locations to include fairy pools both in dungeons and in caves. + * Enemy drop indicator (blue square) now works in caves based on entrance used + * Fixes: + * Collection rate counter is properly hidden in mystery seeds + * Sprite limit lowered where possible to allow for lifting of pots + * Hovers in Swamp Waterway properly do not drop items anymore + * Lots more bans (thanks to jsd in particular but also thanks to all the reports) + * Minor issue with customizer/mystery files not allowing "true" for booleans +* 1.3.0.2v + * Fix for multiworld received keys not counting correctly + * Fix for multiworld lamps incorrect graphics + * Fix for collection rate decreasing on item "pickup" + * Fix for pendants as prizes counting as items + * Fix for castle barrier gfx in rain state + * Enemizer fixes and bans: + * Fixed a generation issue where ChainChomp placement would cause a failure. (Invincible enemies banned in Sprial Cave for early game traversal for now) + * Skull Pot Prison should not be blocked by "impassable" enemies + * Bumpers banned in Ice Hookshot room + * Fixed issue in GT Spike Crystal room + * Fixed blockage issues in TT Ambush and Compass rooms + * Forbid Bumper in Fairy Ascension cave; needed to clip into wall weirdly to pass. + * Enemy Drop bans + * Forbid Stals in many places where they cannot be woken up. Behind rails and on top of blocks, for example. + * A couple minor wizzrobes bans because of despawns. + * Enemies over pits and on conveyors near pits have been issued standard bans for falling enemies. Mimics join the ranks here as they don't work well on pits or on conveyors. + * Mimics banned where conveyors touch walls and could clip out unintentionally +* 1.3.0.1v + * Fixed bugs with item duping and disappearing drops + * Fixed multiworld crash + * Fixed assured sword missing when using start inventory (via GUI/CLI) + * Forbid extra statues in Swamp Push Statue room + * Forbid bumpers on OW water + * Forbid Stal on pits + * Text fix on sprite author (thanks Synack) +* 1.2.0.23u + * Generation: fix for bunny walk logic taking up too much memory + * Key Logic: Partial is now the new default +* 1.2.0.22u + * Flute can't be activated in rain state (except glitched modes) (Thanks codemann!) + * ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used) + * Logic issues: + * Self-locking key not allowed in Sanctuary in standard (typo fixed) + * More advanced bunny-walking logic in dungeons (multiple paths considred) + * MSU: GTBK song fix for DR (Thanks codemann!) +* 1.2.0.21u + * Fix that should force items needed for leaving Zelda's cell to before the throne room, so S&Q isn't mandatory + * Small fix for Tavern Shuffle (thanks Catobat) + * Several small generation fixes +* 1.2.0.20u + * New generation feature that allows Spiral Stair to link to themselves (thank Catobat) + * Added logic for trap doors that could be opened using existing room triggers + * Fixed a problem with inverted generation and the experimental flag + * Added a notes field for user added notes either via CLI or Customizer (thanks Hiimcody and Codemann) + * Fixed a typo for a specific pot hint + * Fix for Hera Boss music (thanks Codemann) +* 1.1.6 (from Stable) + * Minor issue with dungeon counter hud interfering with timer +* 1.2.0.19u + * Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat) + * Fixed a bug with dungeon generation + * Multiworld: Fixed /missing command to not list all the pots + * Changed the "Ganonhunt" goal to use open pyramid on the Auto setting + * Customizer: Fixed the example yaml for shopsanity +* 1.2.0.18u + * Fixed an issue with pyramid hole being in logic when it is not opened. + * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) + * Fix for Hera Boss music (thanks Codemann) + * Fixed an issue where certain vanilla door types would not allow other types to be placed. + * Customizer: fixed an issue where last ditch placements would move customized items. Those are now locked and the generation will fail instead if no alternatives are found. + * Customizer: fixed an issue with assured sword and start_inventory + * Customizer: warns when trying to specifically place an item that's not in the item pool + * Fixed "accessibility: none" displaying a spoiling message + * Fixed warning message about custom item pool when it is fine +* 1.2.0.17u + * Fixed logic bug that allowed Pearl to be behind Graveyard Cave or King's Tomb entrances with only Mirror and West Dark World access (cross world shuffles only) + * Removed backup locations for Dungeon Only and Major Only algorithms. If item cannot be placed in the appropriate location, the seed will fail to generate instead + * Fix for Non-ER Inverted Experimental (Aga and GT weren't logically swapped) + * Fix for customizer setting crystals to 0 for either GT/Ganon +* 1.2.0.16u + * Fix for partial key logic on vanilla Mire + * Fix for Kholdstare Shell collision when at Lanmo 2 + * Fix for Mire Attic Hint door (direction was swapped) + * Dungeon at Chest Game displays correctly on OW map option +* 1.2.0.15u + * GUI reorganization + * Logic fix for pots in GT conveyor cross + * Auto option for pyramid open (trinity or ER + crystals goal) + * World model refactor (combining inverted and normal world models) + * Partitioned fix for lamp logic and links house + * Fix starting flute logic + * Reduced universal keys in pool slightly for non-vanilla dungeons + * Fake world fix finally + * Some extra restrictions on links house placement for lite/lean + * Collection_rate works in customizer files +* 1.2.0.14u + * Small fix for key logic validation (got rid of a false negative) + * Customized doors in ice cross work properly now +* 1.2.0.13u + * Allow green/blue potion refills to be customized + * OW Map showing dungeon entrance at Snitch Lady (West) fixed (instead of @ HC Courtyard) + * Standing item data is cleared on transition to overworld (enemy drops won't bleed to overworld sprites) + * Escape assist won't give you a free quiver in retro bow mode + * Fixed an issue where a door would be opened magically (due to original pairing) + * MultiServer can now disable forfeits if desired +* 1.2.0.12u + * Fix for mirror portal in inverted + * Yet another fix for blocked door in Standard ER +* 1.2.0.11u + * Fixed an issue with lower layer doors in Standard + * Fix for doors in cave state (will no longer be vanilla) + * Added a logic rule for th murderdactyl near bumper ledge for OHKO purposes + * Enemizer alteration for Hovers and normal enemies in shallow water + * Fix for beemizer including modes with an increased item pool + * Fix for district algorithm +* 1.2.0.10u + * Fixed overrun issues with edge transitions + * Better support for customized start_inventory with dungeon items + * Colorized pots now available with lottery. Default is on. + * Dungeon_only support pottery + * Fix AllowAccidentalGlitches flag in OWG + * Potential fix for mirror portal and entering cave on same frame + * A few other minor issues, generation and graphical +* 1.2.0.9-u + * Disallowed standard exits (due to ER) are now graphically half blocked instead of missing + * Graphical issues with Sanctuary and Swamp Hub lobbies are fixed + * Fixes an issue surrounding door state and decoupled doors leading to blocked doors + * Customizer improvements: + * Better logic around customized lobbies + * Better logic around customized door types + * Fix to key doors that was causing extra key doors + * Generation improvement around crystal switches + * Fix bug in dungeon_only that wasn't using pot key locations (known issue still exists in pottery modes) + * Fixes for multiworld: + * Fixes an issue when keys are found in own dungeon for another player when using the bizhawk plugin. + * Fixes an issue with absorbables for another player also being received by the player picking it up. +* 1.2.0.8-u + * New Features: trap_door_mode and key_logic_algorithm + * Change S&Q in door shuffle + standard during escape to spawn as Uncle + * Fix for vanilla doors + certain ER modes + * Fix for unintentional decoupled door in standard + * Fix a problem with BK doors being one-sided + * Change to how wilds keys are placed in standard, better randomization + * Removed a Triforce text + * Fix for Desert Tiles 1 key door +* 1.2.0.7-u + * Fix for some misery mire key logic + * Minor standard generation fix + * Fix for inactive flute start + * Settingsfile for multiworld generation support + * Fix for duped HC/AT Maps/Compasses +* 1.2.0.6-u + * Fix for light cone in Escape when entering from Dark World post-zelda + * Fix for light cone in Escape when lighting a torch with fire rod +* 1.2.0.5.u + * Logic fix for Sanctuary mirror (it wasn't resetting the crystal state) + * Minor bugfixes for customizer +* 1.2.0.4-u + * Starting inventory fixes if item not present in the item pool. + * Support for Assured sword setting and OWG Boots when using a custom item pool. (Customizer or GUI) + * Logic fix for the skull woods star tile that lets you into the X pot room. Now accounts for small key or big key door there blocking the way from the star tile. A trap door is not allowed there. + * Standard logic improvement that requires a path from Zelda to the start so that you cannot get softlocked by rescuing Zelda. Standard mirror scroll change may need to be reverted if impossible seed are still generated. +* 1.2.0.3-u + * Starting inventory taken into account with default item pool. (Custom pools must do this themselves) + * Fast ROM update + * Fix for restricted boss item counting maps & compasses as vital + * Bug fix for vanilla ER + inverted + experimental +* 1.2.0.2-u + * Fixed a bug with certain trap doors missing + * Added a hint reference for district hints +* 1.2.0.1-u + * Added new ganonhunt and completionist goals + * Fixed the issue when defeating Agahnim and standing in the doorway can cause door state to linger. + * Fix for Inverted Lean/Lite ER + * Fix for vanilla Doors + Standard + ER + * Added a limit per dungeon on small key doors to ensure reasonable generation + * Fixed many small bugs + +# Known Issues + +* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed. +* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall is shuffled there. A bomb jump to get to those pot may be required if you don't have boots to bonk across. \ No newline at end of file diff --git a/README.md b/README.md index a70372d5..437d2fcc 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ Option that controls whether enemies that drop items are randomized or not. * None: Special enemies drop keys normally * Keys: Enemies that drop keys are added to the randomization pool. Includes the Hyrule Castle Big Key. Universal adds generic keys to the pool instead. -* Underworld: Enemies in the underworld are added to the randomization pool. Vanilla drops from the various drop tables are added to the item pool. In caves and in dungeons while you have the compass, a blue square will indicate if there are any enemies on the supertile that still have an available drop in the dungeon. Certain enemies do have logical requirements. In particular, the Red Bari requires Fire Rod or Bombos to collect it's drop. More information in hte [Enemizer](#enemizer) section. +* Underworld: Enemies in the underworld are added to the randomization pool. Vanilla drops from the various drop tables are added to the item pool. In caves and in dungeons while you have the compass, a blue square will indicate if there are any enemies on the supertile that still have an available drop in the dungeon. Certain enemies do have logical requirements. In particular, the Red Bari requires Fire Rod or Bombos to collect its drop. More information in the [Enemizer](#enemizer) section. CLI: `--dropshuffle [none|keys|underworld]` @@ -518,7 +518,19 @@ Please see [Customizer documentation](docs/Customizer.md) on how to create custo ### New Modes * Lite: Non item entrances are vanilla. + - Dungeon and multi-entrance caves can only lead to dungeon and multi-entrance caves + - Dropdowns can only lead to dropdowns, with them staying coupled to their appropriate exits + - Cave entrances that normally lead to items can only lead to caves that have items (this includes Potion Shop and Big Bomb Shop) + - All remaining entrances remain vanilla + - Multi-entrance caves are connected same-world only + - LW is guaranteed to have HC/EP/DP/ToH/AT and DW: IP/MM/TR/GT + - Shop locations are included in the Item Cave pool if Shopsanity is enabled + - Caves with pots are included in the Item Cave pool if Pottery is enabled + - Caves with enemies/fairies are included in the Item Cave pool if Shuffle Enemy Drops is enabled * Lean + - Same grouping/pooling mechanism as in Lite ER + - Both dungeons and connectors can be cross-world connections + - No dungeon guarantees like in Lite ER * Swapped: Entrances are swapped with each other ### Shuffle Links House @@ -529,6 +541,46 @@ In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now contr You may shuffle the back of tavern entrance in ER modes when Experimental Features are turned on. +### Skull Woods Shuffle + +In an effort to reduce annoying Skull Woods layouts, several new options have been created. + +- Original: Skull woods shuffles classically amongst itself unless insanity is the mode. This should mimic prior behavior. +- Restricted (Vanilla Drops, Entrances Restricted): Skull woods drops are vanilla. Skull woods entrances stay in skull woods and are shuffled. +- Loose (Vanilla Drops, Entrances use Shuffle): Skull woods drops are vanilla. The main ER mode's pool determines how to handle. +- Followlinked (Follow Linked Drops Setting): This looks at the new linked drop settings. If linked drops are turned on, then two new pairs of linked drop down and holes are formed. Skull front and the hole near the big chest form a pair. The east entrance to Skull 2 and th hole in the back of skull woods form another pair. If the mode is not a cross-world shuffle, then these 2 new drop-down pairs are limited to the dark world. The other drop-down in skull woods, the front two holes will be vanilla. If linked drops are off, then the mode determines how to handle the holes and entrances. + +### Linked Drops Override + +This controls whether drops should be linked to nearby entrances or not. + +- Unset: This uses the mode's default which is considered linked for all modes except insanity +- Linked: Forces drops to be linked to their entrances. +- Independent: Decouples drops from their entrances. In same-world shuffles, holes & entrances may be restricted to a singe world depending on settings and placement to prevent cross-world connection through holes and/or entrances in dungeons. + +### Brief Explanations + +Loose Skull Woods Shuffle: + +- Simple dungeons modes will attempt to fix the layout of skull woods to be more vanilla. This includes dungeonssimple, simple, and restricted ER modes. +- The dungeonsfull mode allows skull woods to be used as a connector but attempt to maintain same-world connectivity. +- Same world modes like lite & full will generally keep skull woods entrances to a single world to prevent cross-world connections. If not inverted, this is not guaranteed to be the dark world though. +- Cross-world modes will be eaiser to comprehend due to fewer restrictions like crossed, lean and swapped. + +Followdrops with Linked Drops: + +- Some modes don't care much about linked drops: simple, dungeonssimple, dungeonsfull +- Same-world modes like restricted, full, & lite will often keep skull woods drop pairs in the dark world and there are only 3 options there: pyramid, and the vanilla locations +- Cross-world modes will benefit the most from the changes as the drop pool expands by two new options for drop placement and guarantees a way out from skull woods west, though the connector must be located. +- Insanity with linked drops will kind of allow a player to scout holes, at the cost of not being to get back to the hole immediately. + +Followdrops with Independent Drops: +- dungeonssimple will place holes vanilla anyway +- dungeonsfull will shuffle the holes +- Same-world modes like simple, restricted, full, & lite will likely pull all skull woods entrances to a single world. (It'll likely be the light world if a single hole is in the light world, unless inverted, then the reverse.) +- Cross-world modes like swapped, lean, and crossed will mean drops are no longer scoutable. Enjoy your coin flips! +- This is insanity's default anyway, no change. + ### Overworld Map Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower. @@ -545,13 +597,21 @@ CLI ```--overworld_map [default|compass|map]``` ## Enemizer -Enemizer has been incorporated into the generator adn no longer requires an external program. However, there are differences. +Enemizer has been incorporated into the generator and no longer requires an external program. However, there are differences. -A document hightlighting the major changes: [Enemizer in DR](https://docs.google.com/document/d/1iwY7Gy50DR3SsdXVaLFIbx4xRBqo9a-e1_jAl5LMCX8/edit?usp=sharing) +Please see this document for extensive details: [Enemizer in DR](https://docs.google.com/document/d/1iwY7Gy50DR3SsdXVaLFIbx4xRBqo9a-e1_jAl5LMCX8/edit?usp=sharing) + +Notable differences: + +* Several sprites added to the pool. Most notable is how enemies behave on shallow water. They work now. +* Clearing rooms, spawnable chests, and enemy keys drops can now have enemies with specific logic in the room. This logic is controlled by the new [Enemy Logic option](#enemy-logic) +* New system for banning enemies that cause issues is in place. If you see an enemy in a place that would cause issue, please report it and it can be banned to never happen again. Current bans can be found [in the code](source/enemizer/enemy_deny.yaml) for the curious +* Thieves are always unkillable, but banned from the entire underworld. We can selectively ban them from problematic places in the overworld, and if someone wants to figure out where they could be safe in the underworld, I'll allow them there once the major problems have been banned. +* Tile room patterns are currently shuffled with enemies. ### Enemy Shuffle -Shuffling enemies is different as there are places that certain enemies are not allowed to go. This is known as the enemy ban list, and is updated regularly as report of poor enemy placement occurs. Poor enemy placement included Bumpers, Statues, Beamos or other enemies that block your path with or without certain items. Other disallowed placements include unavoidable damage and glitches. Thieves are unkillable, but restricted to the overworld and even then, are banned from narrow locations. +Shuffling enemies is different as there are places that certain enemies are not allowed to go. This is known as the enemy ban list, and is updated regularly as reports of poor enemy placement occurs. Poor enemy placement included Bumpers, Statues, Beamos or other enemies that block your path with or without certain items. Other disallowed placements include unavoidable damage and glitches. Thieves are unkillable, but restricted to the overworld and even then, are banned from narrow locations. ### Enemy Damage diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 04aea2df..217ebe12 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,536 +1,9 @@ # New Features -FastROM changes have been included now. - -## Enemizer Features - -Please see this document for extensive details. [Enemizer in DR](https://docs.google.com/document/d/1iwY7Gy50DR3SsdXVaLFIbx4xRBqo9a-e1_jAl5LMCX8/edit?usp=sharing) - -Key points: -* Enemizer no longer uses a third party program. It is now built-in. -* New option under Shuffle Enemy Drops: Underworld. Any underworld enemy can drop items. -* New option under Enemizer tab: Enemy Logic - -Please read the entire document above for extensive details about enemizer and enemy drop shuffle systems. - -Enemizer main changes: -* Several sprites added to the pool. Most notable is how enemies behave on shallow water. They work now. -* Clearing rooms, spawnable chests, and enemy keys drops can now have enemies with specific logic in the room. This logic is controlled by the new Enemy Logic option -* New system for banning enemies that cause issue is place. If you see an enemy in a place that would cause issue, please report it and it can be banned to never happen again. Initial bans can be found [in the code](source/enemizer/enemy_deny.yaml) for the curious -* Thieves are always unkillable, but banned from the entire underworld. We can selectively ban them from problematic places in the overworld, and if someone wants to figure out where they could be safe in the underworld, I'll allow them there once the major problems have been banned. -* THe old "random" and "legacy" options have been discarded for enemy shuffle. Tile room patterns are currently shuffled with enemies. - -Underworld drops: - -* A flashing blue square added to help locate enemies that have remaining drops on the supertile. (Dungeons and caves without a compass get this for free.) -* Flying enemies, spawned enemies, and enemies with special death routines will not drop items. -* Pikits do not drop their item if they have eaten a shield. -* Hovers in swamp waterway do no drop items due to a layer issue that's not been solved. -* Enemies that are over pits require boomerang or hookshot to collect the item -* Enemies behind rails require the boomerang (hookshot can sequence break in certain cases) -* Enemies that spawn on walls do not drop items. (Keese normally don't, but in enemizer these can be valid drops otherwise. The document has a visual guide.) - -(Older notes below) - -One major change with this update is that big key doors and certain trap doors are no longer guaranteed to be vanilla in Dungeon Door Shuffle modes even if you choose not to shuffle those types. A newer algorithm for putting dungeons together has been written and it will remove big key doors and trap doors when necessary to ensure progress can be made. - -Please note that retro features are now independently customizable as referenced below. Selecting Retro mode or World State: Retro will change Bow Mode to Retro (Progressive). Take Anys to Random, and Small Keys to Universal. - -## Flute Mode - -Normal mode for flute means you need to activate it at the village statue after finding it like usual. -Activated flute mode mean you can use it immediately upon finding it. the flute SFX plays to let you know this is the case. - -## Bow Mode - -Four options here: - -* Progressive. Standard progressive bows. -* Silvers separate. One bow in the pool and silvers are a separate item. -* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places. -* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found). - -## Dungeon Shuffle Features - -### Small Keys - -There are three options now available: - -* In Dungeon: The small key will be in their own dungeon -* Randomized: Small keys can be shuffled outside their own dungeon -* Universal: Retro keys without the other options - -### Dungeon Door Shuffle - -New mode: Partitioned. Partly between basic and crossed, dungeons are shuffled in 3 pools: - -* Light World dungeons, Hyrule Castle and Aga Tower are mixed together -* Palace of Darkness, Swamp Palace, Skull Woods, and Thieves Town are mixed together -* The other dark world dungeons including Ganons Tower are mixed together - -### Door Types to Shuffle - -Four options here, and all of them only take effect if Dungeon Door Shuffle is not Vanilla: - -* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously -* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change -* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled. -* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more. - -Note: Boss Trap doors are removed currently and not added into the trap door pool as extra trap doors. This may not be a permanent change - -### Decouple Doors - -This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions. - -## Customizer - -Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds. - -## New Goals - -### Ganonhunt -Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI - -### Completionist -All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon. - - -## Standard Generation Change - -Hyrule Castle in standard mode is generated a little differently now. The throne room is guaranteed to be in Hyrule Castle and the Sanctuary is guaranteed to be beyond that. Additionally, the Mirror Scroll will bring you back to Zelda's Cell or the Throne Room depending on what save point you last obtained, this is to make it consistent with where you end up if you die. If you are lucky enough to find the Mirror, it behaves differently and brings you the last entrance used - giving you more options for exploration in Hyrule Castle. - -## ER Features - -### New Experimental Algorithm - -To accommodate future flexibility the ER algorithm was rewritten for easy of use. This allows future modes to be added more easily. This new algorithm is only used when the experimental flag is turned on. - -### Lite/Lean ER (Experimental required) - -Designed by Codemann, these are available now (only with experimental turned on - they otherwise fail) - -#### Lite -- Dungeon and multi-entrance caves can only lead to dungeon and multi-entrance caves -- Dropdowns can only lead to dropdowns, with them staying coupled to their appropriate exits -- Cave entrances that normally lead to items can only lead to caves that have items (this includes Potion Shop and Big Bomb Shop) -- All remaining entrances remain vanilla -- Multi-entrance caves are connected same-world only -- LW is guaranteed to have HC/EP/DP/ToH/AT and DW: IP/MM/TR/GT -- Shop locations are included in the Item Cave pool if Shopsanity is enabled -- Houses with pots are included in the Item Cave pool if Pottery is enabled - -#### Lean -- Same grouping/pooling mechanism as in Lite ER -- Both dungeons and connectors can be cross-world connections -- No dungeon guarantees like in Lite ER - -### Skull Woods Shuffle - -In an effort to reduce annoying Skull Woods layouts, several new options have been created. - -- Original: Skull woods shuffles classically amongst itself unless insanity is the mode. This should mimic prior behavior. -- Restricted (Vanilla Drops, Entrances Restricted): Skull woods drops are vanilla. Skull woods entrances stay in skull woods and are shuffled. -- Loose (Vanilla Drops, Entrances use Shuffle): Skull woods drops are vanilla. The main ER mode's pool determines how to handle. -- Followdrops (Follow Linked Drops Setting): This looks at the new linked drop settings. If linked drops are turned on, then two new pairs of linked drop down and holes are formed. Skull front and the hole near the big chest form a pair. The east entrance to Skull 2 and th hole in the back of skull woods form another pair. If the mode is not a cross-world shuffle, then these 2 new drop-down pairs are limited to the dark world. The other drop-down in skull woods, the front two holes will be vanilla. If linked drops are off, then the mode determines how to handle the holes and entrances. - -### Linked Drops Override - -This controls whether drops should be linked to nearby entrances or not. - -- Unset: This uses the mode's default which is considered linked for all modes except insanity -- Linked: Forces drops to be linked to their entrances. -- Independent: Decouples drops from their entrances. In same-world shuffles, holes & entrances may be restricted to a singe world depending on settings and placement to prevent cross-world connection through holes and/or entrances in dungeons. - -### Brief Explanations - -Loose Skull Woods Shuffle: - -- Simple dungeons modes will attempt to fix the layout of skull woods to be more vanilla. This includes dungeonssimple, simple, and restricted ER modes. -- The dungeonsfull mode allows skull woods to be used as a connector but attempt to maintain same-world connectivity. -- Same world modes like lite & full will generally keep skull woods entrances to a single world to prevent cross-world connections. If not inverted, this is not guaranteed to be the dark world though. -- Cross-world modes will be eaiser to comprehend due to fewer restrictions like crossed, lean and swapped. - -Followdrops with Linked Drops: - -- Some modes don't care much about linked drops: simple, dungeonssimple, dungeonsfull -- Same-world modes like restricted, full, & lite will often keep skull woods drop pairs in the dark world and there are only 3 options there: pyramid, and the vanilla locations -- Cross-world modes will benefit the most from the changes as the drop pool expands by two new options for drop placement and guarantees a way out from skull woods west, though the connector must be located. -- Insanity with linked drops will kind of allow a player to scout holes, at the cost of not being to get back to the hole immediately. - -Followdrops with Independent Drops: -- dungeonssimple will place holes vanilla anyway -- dungeonsfull will shuffle the holes -- Same-world modes like simple, restricted, full, & lite will likely pull all skull woods entrances to a single world. (It'll likely be the light world if a single hole is in the light world, unless inverted, then the reverse.) -- Cross-world modes like swapped, lean, and crossed will mean drops are no longer scoutable. Enjoy your coin flips! -- This is insanity's default anyway, no change. - - -### Back of Tavern Shuffle (Experimental required) - -Thanks goes to Catobat which now allows the back of tavern to be shuffled anywhere and any valid cave can be at the back of tavern with this option checked. Available in experimental only for now as it requires the new algorithm to be shuffled properly. - -#### Take Any Caves - -These are now independent of retro mode and have three options: None, Random, and Fixed. None disables the caves. Random works as take-any caves did before. Fixed means that the take any caves replace specific fairy caves in the pool and will be at those entrances unless ER is turned on (then they can be shuffled wherever). The fixed entrances are: - -* Desert Healer Fairy -* Swamp Healer Fairy (aka Light Hype Cave) -* Dark Death Mountain Healer Fairy -* Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb) -* Bonk Fairy (Dark) +None for this release. # Patch Notes * 1.4.3 - * Generation: Fixed several generation problems with ER and intensity 3 -* 1.4.2 - * New ER Options: - * [Skull Woods shuffle options](#skull-woods-shuffle) - * [New option](#linked-drops-override) to override linked drop down behavior - * Customizer: You can now start with a "RandomWeapon" in the start inventory section - * Customizer: You may now start with "Big Magic" or "Small Magic" items - * Customizer: Suppress warning for missing items if they are in start inventory - * MultiClient: change default port to 23074 for newer SNI versions - * Generation: Fixed typo causing ER gen failure with Bomb Shop at Graveyard Ledge -* 1.4.1.12u - * New Entrance Shuffle Algorithm no longer experimental - * Back of Tavern Shuffle now on by default - * Enemizer: Wallmasters banned from tiles where spiral staircases are. (Softlock issue) - * Packaged build of unstable now available - * Customizer: New PreferredLocationGroup for putting a set of items in a set of locations. See customizer docs. - * Customizer: Fixed an issue with starting with `Ocarina` and flute_mode is active - * Spoiler: Some reformatting. Crystal req. for GT/Ganon moved to requirements section so randomized requirements don't show up in the meta section - * Algorithm: Major_Only. Supports up to 16 extra locations (the visible heart pieces) for when major item count exceeds major location count. Examples: Triforce Hunt, Trinity (Triforce on Ped), Bombbag shuffle - * Fix: HC Big Key drop doesn't count on Basic Doors - * Fix: Small Key for this dungeon in Hera Basement doesn't count twice for the key counter - * Fix: All cross-dungeon modes with restrict boss items should require map/compass for the boss - * Fixed a small bug with traversal algorithm - * Enemizer: Enemy bans+ -* 1.4.1.11u - * New Feature: Several spoiler levels added: None, Settings-only, Semi, Full, Debug - * Semi includes only entrances, prizes, and medallions (potential new spoiler mode being worked on, definition may change) - * Entrance: Lite/Lean support enemy drop shuffle - * Standard: Re-added tutorial guard near large rock - * Enemizer - * Fixed the overwriting of bonk fairies - * Fixed broken graphics on hyrule castle - * Enemy bans - * Customizer: Fixed bug with customizing prize packs -* 1.4.1.10u - * Vanilla key logic: Fix for vanilla layout Misery Mire which allows more complex key logic. Locations blocked by crystal switch access are only locked by 2 keys thanks to that being the minimum in Mire to reach one of two crystal switches. - * Autotracking: Fix for chest turn counter with chest containing multiworld items (Thanks Hiimcody) - * Enemizer: Enemy bans - * Rom: Code prettification and fixing byte designations by Codemann - * Support added for BPS patches via jsonout setting (Thanks Veetorp!) -* 1.4.1.9u - * Enemy Drop Underworld: Changed enemy drop indicator to not require compass - * Experimental: Moved dark world bunny spawns out of experimental. (It is now always on) - * Fix: Red/Blue pendants were swapped for autotracking. (Thanks Muffins!) - * Fix: Red square sometimes wasn't blinking - * Updated tournament winners - * Enemizer: Enemy bans -* 1.4.1.8u - * HUD: New dungeon indicators based on common abbreviations - * OWG+HMG: EG is allowed to be armed - * Drop Shuffle: Fixed an issue with minor drops counting for the wrong dungeon - * Enemizer: Trinexx ice breath should be properly disabled if Trinexx is located outside of Turtle Rock - * Enemizer: Enemy bans -* 1.4.1.7u - * Some bugs around Triforce Pieces smoothed out - * Enemizer: No exception for mimics/eyegores in vanilla rooms if enemy logic is turned to off - * Enemizer: Various enemy bans -* 1.4.1.6u - * Difficulty: Fixed some issues around item caps not being respected - * Enemezier: Tutorial guards remove from South Kakariko - * Map: Pendant colors fixed - * Minor rom code cleanup - * Enemizer: Hovers added to problematic pool near pits. Some other bans -* 1.4.1.5u - * Major Fix: Problem with Ganon's Room sprites - * HMG: Remove extra Swamp Smalls in the pool due to pottery settings - * Enemizer: Couple enemy bans -* 1.4.1.4u - * Logic: Fixed logic bugs surrounding dynammic doors missing logic from big keys and other door types - * Inverted: Castle warp should not appear after defeating Aga 1 - * Enemzier: Fixed a crash with cached sprites Zora/Swamola -* 1.4.1.3v - * Enemizer: Raven/Murderdactyls using the correct damage table - * Enemzier: Boss drops only center when boss shuffle is on -* 1.4.1.2v - * Expert/Hard Item Pool: Capacity fairy no longer gives out free crystal - * Vanilla door + Universal Keys: Generation fixed - * Boss Shuffle: Generation fixed (thanks Codemann for easy solution) - * Vanilla ER: No need for ability to check prizes on keysanity menu - * Swapped ER: Possible generation issue fixed (thanks Codemann) - * Enemizer: Roller ban - * Performance: Faster text boxes. Thanks Kan! -* 1.4.1.1v - * Logic: Moon pearl logic respects blocked doors -* 1.4.1.0v - * World Model Refactor: The overworld has been split up by screen, brings OR and DR a bit closer together in the model sense. A few OWG clips have been rewritten to fit into this new logic better. - * Logic: New logic for some bosses on ice - * Helmasaur on Ice: Bombs for mask, sword or arrows for 2nd phase - * Blind on Ice: Beam sword, Somaria, or Byrna plus magic extension for damage. Red shield or Byrna for protection. - * Kholdstare on Ice: Three options (after cracking the shell) - * Beam sword - * Fire Rod with 1.5 magic extensions - * Fire Rod & Bombos & any Sword & 1 Magic Extension - * Vitreous on Ice: Arrows and Bombs or a Beam Sword - * Trinexx on Ice: Boots always required for dodging. Damage options: - * Gold sword - * Tempered sword with magic extension - * Hammer or Master sword with 3 magic extensions (Rod spam for elemental heads, non-ideal weapon for last phase) - * Trinexx on Ice forbidden in doors seeds until we can model some health requirements. Low health Trinexx still isn't realistically feasible (bascially playing OHKO) - * Logic: Added silver arrows as Arrghus damage option when item functionality is not set to hard or expert - * Logic: Byrna not in logic for laser bridge when item functionality is set to hard or expert - * Enemizer Damage Rework: - * Shuffled: Actually shuffles the damage groups in the table instead of picking random numbers and reducing for mails from there. Enemies will still be assigned to a damage group randomly. - * There will always be at least one group which does no damage. The thief will always be in that group. Ganon always has his own group. - * Glitched modes: Aga 1 should be vulnerable in rain state for glitched modes - * Generation: Trinexx and Lanmolas room allowed as lobbies in intensity 3 (works with enemizer now) - * Enemy AI: Terrorpin AI code removed. May help with unusual enemy behavior? -* 1.4.0.1v - * Key logic: Vanilla key logic fixes. Statically set some HC logic and PoD front door - * Generation: Fix a broken tile pattern - * Inverted: Castle warp should not appear after defeating Aga 1 - * Murahdahla: Should not disappear after Aga 1. May fix other subtle issues. - * Shopsanity: Buying multiple of an item in the potion shop should no longer increase item count. -* 1.4.0.0v - * Initial support for HMG (Thanks Muffins!) - * Generation: fix for bunny walk logic taking up too much memory - * Key Logic: Partial is now the new default - * Enemizer: enemy bans -* 1.3.0.9v - * ER: New Swapped ER mode borrowed from OWR - * ER: fixed a generation error where TR chooses all "must-exits" - * Ganonhunt: playthrough no longer collects crystals - * Vanilla Fill: Uncle weapon is always a sword, medallions for Mire/TR will be vanilla - * Customizer: support shufflebosses/shuffleenemies as well as boss_shuffle/enemy_shuffle - * Enemizer: enemy bans -* 1.3.0.8v - * Enemizer: Red Mimics correctly banned from challenge rooms in appropriate logic setting - * No Logic Standard ER: Rain doors aren't blocked if no logic is enabled. - * Trinexx: attempt to fix early start - * MW Progression Balancing: Change to be percentage based instead of raw count. (80% threshold) - * Take anys: Good Bee cave chosen as take any should no longer prevent generation - * Money balancing: Fixed generation issue - * Enemizer: various enemy bans -* 1.3.0.7v - * Fix for Mimic Cave enemy drops - * Fix for Spectacle Rock Cave enemy drops (the mini-moldorms) - * Fix for multiworld lamps with incorrect graphics - * No longer shuffles fairy bonks (from trees) as part of Enemizer -* 1.3.0.6v - * Flute can't be activated in rain state (except glitched modes) (Thanks codemann!) - * Enemizer - * Arrghus at Lanmo 2 no longer prevents pot pickups - * Trinexx at Lanmo 2 requires the Cape go backwards to face him - * Lift-able Blocks require a sprite slot (should help reduce problems) - * Fixed logic issues: - * Self-locking key not allowed in Sanctuary in standard (typo fixed) - * More advanced bunny-walking logic in dungeons (multiple paths considered) - * ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used) - * MSU: GTBK song fix for DR (Thanks codemann!) - * District Algorithm: Fails if no available location outside chosen districts - * Various enemy bans - * More Gibos near kiki and Old Man - * Bumper/AntiFairy obstacles - * Damaging roller - * Statue + Pots don't mix - * Statues on Skull Big Key Chest tile - * Toppo in challenge rooms - * Misc others -* 1.3.0.5v - * Hud/Map Counter: Collecting a keys for this dungeon of a bonk torch no longer increments the counter twice and immediately updates the hud. - * Enemizer: Hera basement item counting twice fixed by banning wallmasters on the tile. - * Enemizer: Statues banned offscreen for pull switches - * Enemizer: Several sprite producing enemies have been limited on crowded tiles. Offenders: Hinox, Sluggula, Bomb Guard, Beamos, Gibo, Wall Cannons, Probe using Guards. Others do not spam as many projectiles. - * Enemizer: More enemy bans (mostly Wizzrobes near walls where they won't spawn, couple missed firebar spots) -* 1.3.0.4v - * Enemizer: The bunny beam near Lanmo 2 and the 4 fairies near Ice Armos are not shuffled anymore. This is due to how bosses shuffle works and since it cannot be guaranteed to work within the current system, they are vanilla. (Vitreous still overwrites the fairies and Arrghus only lets two spawn, etc.) - * Dropshuffle: Pokey 1 has been fixed to drop his item - * Mystery/Customizer: true/false and on/off in yaml files should behave the same. - * More enemy bans as have been reported -* 1.3.0.3v - * Faeries now part of the enemy shuffle pool. Take note, this will increase enemy drop locations to include fairy pools both in dungeons and in caves. - * Enemy drop indicator (blue square) now works in caves based on entrance used - * Fixes: - * Collection rate counter is properly hidden in mystery seeds - * Sprite limit lowered where possible to allow for lifting of pots - * Hovers in Swamp Waterway properly do not drop items anymore - * Lots more bans (thanks to jsd in particular but also thanks to all the reports) - * Minor issue with customizer/mystery files not allowing "true" for booleans -* 1.3.0.2v - * Fix for multiworld received keys not counting correctly - * Fix for multiworld lamps incorrect graphics - * Fix for collection rate decreasing on item "pickup" - * Fix for pendants as prizes counting as items - * Fix for castle barrier gfx in rain state - * Enemizer fixes and bans: - * Fixed a generation issue where ChainChomp placement would cause a failure. (Invincible enemies banned in Sprial Cave for early game traversal for now) - * Skull Pot Prison should not be blocked by "impassable" enemies - * Bumpers banned in Ice Hookshot room - * Fixed issue in GT Spike Crystal room - * Fixed blockage issues in TT Ambush and Compass rooms - * Forbid Bumper in Fairy Ascension cave; needed to clip into wall weirdly to pass. - * Enemy Drop bans - * Forbid Stals in many places where they cannot be woken up. Behind rails and on top of blocks, for example. - * A couple minor wizzrobes bans because of despawns. - * Enemies over pits and on conveyors near pits have been issued standard bans for falling enemies. Mimics join the ranks here as they don't work well on pits or on conveyors. - * Mimics banned where conveyors touch walls and could clip out unintentionally -* 1.3.0.1v - * Fixed bugs with item duping and disappearing drops - * Fixed multiworld crash - * Fixed assured sword missing when using start inventory (via GUI/CLI) - * Forbid extra statues in Swamp Push Statue room - * Forbid bumpers on OW water - * Forbid Stal on pits - * Text fix on sprite author (thanks Synack) -* 1.2.0.23u - * Generation: fix for bunny walk logic taking up too much memory - * Key Logic: Partial is now the new default -* 1.2.0.22u - * Flute can't be activated in rain state (except glitched modes) (Thanks codemann!) - * ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used) - * Logic issues: - * Self-locking key not allowed in Sanctuary in standard (typo fixed) - * More advanced bunny-walking logic in dungeons (multiple paths considred) - * MSU: GTBK song fix for DR (Thanks codemann!) -* 1.2.0.21u - * Fix that should force items needed for leaving Zelda's cell to before the throne room, so S&Q isn't mandatory - * Small fix for Tavern Shuffle (thanks Catobat) - * Several small generation fixes -* 1.2.0.20u - * New generation feature that allows Spiral Stair to link to themselves (thank Catobat) - * Added logic for trap doors that could be opened using existing room triggers - * Fixed a problem with inverted generation and the experimental flag - * Added a notes field for user added notes either via CLI or Customizer (thanks Hiimcody and Codemann) - * Fixed a typo for a specific pot hint - * Fix for Hera Boss music (thanks Codemann) -* 1.1.6 (from Stable) - * Minor issue with dungeon counter hud interfering with timer -* 1.2.0.19u - * Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat) - * Fixed a bug with dungeon generation - * Multiworld: Fixed /missing command to not list all the pots - * Changed the "Ganonhunt" goal to use open pyramid on the Auto setting - * Customizer: Fixed the example yaml for shopsanity -* 1.2.0.18u - * Fixed an issue with pyramid hole being in logic when it is not opened. - * Crystal cutscene at GT use new symmetrical layouts (thanks Codemann) - * Fix for Hera Boss music (thanks Codemann) - * Fixed an issue where certain vanilla door types would not allow other types to be placed. - * Customizer: fixed an issue where last ditch placements would move customized items. Those are now locked and the generation will fail instead if no alternatives are found. - * Customizer: fixed an issue with assured sword and start_inventory - * Customizer: warns when trying to specifically place an item that's not in the item pool - * Fixed "accessibility: none" displaying a spoiling message - * Fixed warning message about custom item pool when it is fine -* 1.2.0.17u - * Fixed logic bug that allowed Pearl to be behind Graveyard Cave or King's Tomb entrances with only Mirror and West Dark World access (cross world shuffles only) - * Removed backup locations for Dungeon Only and Major Only algorithms. If item cannot be placed in the appropriate location, the seed will fail to generate instead - * Fix for Non-ER Inverted Experimental (Aga and GT weren't logically swapped) - * Fix for customizer setting crystals to 0 for either GT/Ganon -* 1.2.0.16u - * Fix for partial key logic on vanilla Mire - * Fix for Kholdstare Shell collision when at Lanmo 2 - * Fix for Mire Attic Hint door (direction was swapped) - * Dungeon at Chest Game displays correctly on OW map option -* 1.2.0.15u - * GUI reorganization - * Logic fix for pots in GT conveyor cross - * Auto option for pyramid open (trinity or ER + crystals goal) - * World model refactor (combining inverted and normal world models) - * Partitioned fix for lamp logic and links house - * Fix starting flute logic - * Reduced universal keys in pool slightly for non-vanilla dungeons - * Fake world fix finally - * Some extra restrictions on links house placement for lite/lean - * Collection_rate works in customizer files -* 1.2.0.14u - * Small fix for key logic validation (got rid of a false negative) - * Customized doors in ice cross work properly now -* 1.2.0.13u - * Allow green/blue potion refills to be customized - * OW Map showing dungeon entrance at Snitch Lady (West) fixed (instead of @ HC Courtyard) - * Standing item data is cleared on transition to overworld (enemy drops won't bleed to overworld sprites) - * Escape assist won't give you a free quiver in retro bow mode - * Fixed an issue where a door would be opened magically (due to original pairing) - * MultiServer can now disable forfeits if desired -* 1.2.0.12u - * Fix for mirror portal in inverted - * Yet another fix for blocked door in Standard ER -* 1.2.0.11u - * Fixed an issue with lower layer doors in Standard - * Fix for doors in cave state (will no longer be vanilla) - * Added a logic rule for th murderdactyl near bumper ledge for OHKO purposes - * Enemizer alteration for Hovers and normal enemies in shallow water - * Fix for beemizer including modes with an increased item pool - * Fix for district algorithm -* 1.2.0.10u - * Fixed overrun issues with edge transitions - * Better support for customized start_inventory with dungeon items - * Colorized pots now available with lottery. Default is on. - * Dungeon_only support pottery - * Fix AllowAccidentalGlitches flag in OWG - * Potential fix for mirror portal and entering cave on same frame - * A few other minor issues, generation and graphical -* 1.2.0.9-u - * Disallowed standard exits (due to ER) are now graphically half blocked instead of missing - * Graphical issues with Sanctuary and Swamp Hub lobbies are fixed - * Fixes an issue surrounding door state and decoupled doors leading to blocked doors - * Customizer improvements: - * Better logic around customized lobbies - * Better logic around customized door types - * Fix to key doors that was causing extra key doors - * Generation improvement around crystal switches - * Fix bug in dungeon_only that wasn't using pot key locations (known issue still exists in pottery modes) - * Fixes for multiworld: - * Fixes an issue when keys are found in own dungeon for another player when using the bizhawk plugin. - * Fixes an issue with absorbables for another player also being received by the player picking it up. -* 1.2.0.8-u - * New Features: trap_door_mode and key_logic_algorithm - * Change S&Q in door shuffle + standard during escape to spawn as Uncle - * Fix for vanilla doors + certain ER modes - * Fix for unintentional decoupled door in standard - * Fix a problem with BK doors being one-sided - * Change to how wilds keys are placed in standard, better randomization - * Removed a Triforce text - * Fix for Desert Tiles 1 key door -* 1.2.0.7-u - * Fix for some misery mire key logic - * Minor standard generation fix - * Fix for inactive flute start - * Settingsfile for multiworld generation support - * Fix for duped HC/AT Maps/Compasses -* 1.2.0.6-u - * Fix for light cone in Escape when entering from Dark World post-zelda - * Fix for light cone in Escape when lighting a torch with fire rod -* 1.2.0.5.u - * Logic fix for Sanctuary mirror (it wasn't resetting the crystal state) - * Minor bugfixes for customizer -* 1.2.0.4-u - * Starting inventory fixes if item not present in the item pool. - * Support for Assured sword setting and OWG Boots when using a custom item pool. (Customizer or GUI) - * Logic fix for the skull woods star tile that lets you into the X pot room. Now accounts for small key or big key door there blocking the way from the star tile. A trap door is not allowed there. - * Standard logic improvement that requires a path from Zelda to the start so that you cannot get softlocked by rescuing Zelda. Standard mirror scroll change may need to be reverted if impossible seed are still generated. -* 1.2.0.3-u - * Starting inventory taken into account with default item pool. (Custom pools must do this themselves) - * Fast ROM update - * Fix for restricted boss item counting maps & compasses as vital - * Bug fix for vanilla ER + inverted + experimental -* 1.2.0.2-u - * Fixed a bug with certain trap doors missing - * Added a hint reference for district hints -* 1.2.0.1-u - * Added new ganonhunt and completionist goals - * Fixed the issue when defeating Agahnim and standing in the doorway can cause door state to linger. - * Fix for Inverted Lean/Lite ER - * Fix for vanilla Doors + Standard + ER - * Added a limit per dungeon on small key doors to ensure reasonable generation - * Fixed many small bugs - -# Known Issues - -* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed. -* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall is shuffled there. A bomb jump to get to those pot may be required if you don't have boots to bonk across. \ No newline at end of file + * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues + * Generation: Fixed several generation problems with ER and intensity 3 \ No newline at end of file diff --git a/Rules.py b/Rules.py index 26a7c302..b642f50a 100644 --- a/Rules.py +++ b/Rules.py @@ -2327,7 +2327,7 @@ def add_key_logic_rules(world, player): eval_func = eval_small_key_door if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild': eval_func = eval_small_key_door_strict - elif world.key_logic_algorithm[player] != 'default': + elif world.key_logic_algorithm[player] != 'dangerous': eval_func = eval_small_key_door_partial for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): diff --git a/mystery_example.yml b/mystery_example.yml index b7ba403e..726cf8bb 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -27,8 +27,8 @@ boss: 0 oneway: 0 key_logic_algorithm: - default: 1 - partial: 0 + dangerous: 0 + partial: 1 strict: 0 decoupledoors: off door_self_loops: diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index 64129bad..309922bc 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -25,7 +25,7 @@ trap_door_mode: boss: 1 oneway: 1 key_logic_algorithm: - default: 1 + dangerous: 1 partial: 0 strict: 0 decoupledoors: diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 415ab50f..d855857c 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -208,7 +208,7 @@ }, "key_logic_algorithm": { "choices": [ - "default", + "dangerous", "partial", "strict" ] diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 47501d95..113b5efe 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -261,7 +261,7 @@ ], "key_logic_algorithm": [ "Key Logic Algorithm (default: %(default)s)", - "default: Balance between safety and randomization", + "dangerous: Key usage must follow logic for safety", "partial: Partial protection when using certain minor glitches", "strict: Ensure small keys are available" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index a0a5677b..e841d2fa 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -86,7 +86,7 @@ "randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps", "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", - "randomizer.dungeon.key_logic_algorithm.default": "Default", + "randomizer.dungeon.key_logic_algorithm.dangerous": "Dangerous", "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", "randomizer.dungeon.key_logic_algorithm.strict": "Strict", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index 4be6a385..3f20f760 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -2,11 +2,11 @@ "widgets": { "key_logic_algorithm": { "type": "selectbox", - "default": "default", + "default": "partial", "options": [ - "default", "partial", - "strict" + "strict", + "dangerous" ], "config": { "padx": [20,0], diff --git a/test/suite/default_key_logic.yaml b/test/suite/default_key_logic.yaml index e9f3e94b..39b5e43c 100644 --- a/test/suite/default_key_logic.yaml +++ b/test/suite/default_key_logic.yaml @@ -6,7 +6,7 @@ meta: players: 1 settings: 1: - key_logic_algorithm: default + key_logic_algorithm: dangerous keysanity: True crystals_needed_for_gt: 0 # to skip trash fill placements: From fe28c1763726e3368901f16b396801d382ba49ad Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 14 Jun 2024 16:21:40 -0600 Subject: [PATCH 18/28] fix: various enemy bans --- README.md | 4 ++-- RELEASENOTES.md | 3 ++- source/enemizer/enemy_deny.yaml | 15 ++++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 437d2fcc..9bd6aba5 100644 --- a/README.md +++ b/README.md @@ -175,11 +175,11 @@ All other dungeon items can be restricted to their own dungeon or shuffled in th Determines how small key door logic works. -* ~~Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable)~~ (Not recommended to use this setting) * Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches. * Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic. +* Dangerous: Assumes you never use keys out of logic. This is the most dangerous setting and not recommend for use. -CLI: `--key_logic [default|partial|strict]` +CLI: `--key_logic [partial|strict|dangerous]` ### Decouple Doors diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 217ebe12..30ba40f0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,4 +6,5 @@ None for this release. * 1.4.3 * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues - * Generation: Fixed several generation problems with ER and intensity 3 \ No newline at end of file + * Generation: Fixed several generation problems with ER and intensity 3 + * Enemizer: Various enemy bans \ No newline at end of file diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index 65a96ab1..c8383fb3 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -54,10 +54,12 @@ UwGeneralDeny: - [0x0028, 2, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] - [0x0028, 3, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora"]] - [0x0028, 4, ["Raven", "Poe", "GreenZirro", "BlueZirro", "Swamola", "Zora", "RollerVerticalUp"]] #"Swamp Palace - Entrance Ledge - Spike Trap" - - [ 0x002a, 2, [ "SparkCW", "SparkCCW", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper"]] #"Palace of Darkness - Arena Main - Hardhat Beetle 1" - - [ 0x002a, 3, [ "Statue", "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "FirebarCW", "FirebarCCW", "SpikeBlock", "Bumper" ] ] #"Palace of Darkness - Arena Main - Hardhat Beetle 2" + - [0x002a, 1, ["RollerVerticalUp", "RollerVerticalDown"]] + - [0x002a, 2, [ "SparkCW", "SparkCCW", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "Chainchomp"]] #"Palace of Darkness - Arena Main - Hardhat Beetle 1" + - [0x002a, 3, ["Statue", "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "FirebarCW", "FirebarCCW", "SpikeBlock", "Bumper", "Chainchomp"]] #"Palace of Darkness - Arena Main - Hardhat Beetle 2" - [ 0x002a, 4, [ "Statue", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper", "RollerHorizontalRight", "RollerHorizontalLeft"]] - [ 0x002a, 6, [ "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Palace of Darkness - Arena Main - Hardhat Beetle 5" + - [0x002a, 7, ["RollerVerticalUp", "RollerVerticalDown", "Chainchomp"]] - [ 0x002b, 5, [ "RollerHorizontalRight" ] ] #"Palace of Darkness - Fairies - Red Bari 2" - [ 0x002e, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "BigSpike", "FirebarCW", "FirebarCCW" ] ] #"Ice Palace - Penguin Chest - Pengator 1" - [ 0x002e, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "BigSpike", "FirebarCW", "FirebarCCW" ] ] #"Ice Palace - Penguin Chest - Pengator 2" @@ -80,6 +82,7 @@ UwGeneralDeny: - [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper"]] #"Skull Woods - Play Pen - Hardhat Beetle" - [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2" - [ 0x003b, 1, [ "Bumper" ]] + - [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]] - [ 0x003c, 0, ["BigSpike"]] - [ 0x003c, 1, [ "SparkCW", "SparkCCW", "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Hookshot Cave - Blue Bari 1" - [ 0x003c, 2, [ "SparkCW", "SparkCCW", "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Hookshot Cave - Blue Bari 2" @@ -205,7 +208,7 @@ UwGeneralDeny: - [ 0x007b, 7, [ "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - DMs Room - Hardhat Beetle" - [ 0x007c, 1, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Randomizer Room - Fire Bar (Counterclockwise)" - [ 0x007c, 2, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Randomizer Room - Spike Trap" - - [ 0x007c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper", "Statue"]] #"Ganon's Tower - Randomizer Room - Fire Bar (Clockwise)" + - [ 0x007c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper", "Statue", "SpikeBlock"]] #"Ganon's Tower - Randomizer Room - Fire Bar (Clockwise)" - [ 0x007c, 4, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Randomizer Room - Hardhat Beetle" - [ 0x007d, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - The Zoo - Fire Snake 1" - [ 0x007d, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - The Zoo - Fire Snake 2" @@ -267,6 +270,7 @@ UwGeneralDeny: - [ 0x009c, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 3" - [ 0x009c, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 4" - [ 0x009c, 5, [ "RollerVerticalUp", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Invisible Floor Maze - Hardhat Beetle 5" + - [0x009d, 2, ["AntiFairyCircle"]] - [ 0x009d, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ganon's Tower - Compass Room - Gibdo 2" - [ 0x009d, 6, [ "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Ganon's Tower - Compass Room - Blue Bari 1" - [ 0x009d, 7, [ "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ganon's Tower - Compass Room - Blue Bari 2" @@ -288,7 +292,7 @@ UwGeneralDeny: - [ 0x00ab, 7, [ "RollerVerticalUp", "RollerHorizontalLeft" ] ] #"Thieves' Town - Spike Dodge - Spike Trap 6" - [ 0x00ae, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Ice T - Blue Bari 1" - [ 0x00ae, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Ice T - Blue Bari 2" - - [ 0x00af, 0, [ "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Ice Palace - Ice Clock - Fire Bar (Clockwise)" + - [0x00af, 0, ["RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper", "Lynel"]] #"Ice Palace - Ice Clock - Fire Bar (Clockwise)" - [0x00b0, 7, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay - [0x00b0, 8, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay - [ 0x00b1, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 1" @@ -332,6 +336,7 @@ UwGeneralDeny: - [ 0x00cb, 9, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] - [ 0x00cb, 10, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] - [ 0x00cb, 11, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots + - [0x00cc, 6, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper"]] # prevent access around - [ 0x00cc, 8, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #Prevents Pot access (Beamos okay?) - [ 0x00cc, 12, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #Prevents Pot access (Beamos okay?) - [ 0x00ce, 0, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "AntiFairyCircle", "Antifairy", "BigSpike", "FirebarCCW", "Bumper", "Chainchomp"]] #"Ice Palace - Over Boss - top - Red Bari 1" @@ -389,7 +394,7 @@ UwGeneralDeny: - [ 0x00e7, 5, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalRight" ] ] #"Death Mountain Descent Cave Right - Keese 6" - [ 0x00e7, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalRight" ] ] #"Death Mountain Descent Cave Right - Keese 7" - [ 0x00e8, 0, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "SpikeBlock", "Bumper" ] ] #"Super Bunny Exit - Hardhat Beetle 1" - - [ 0x00e8, 1, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Super Bunny Exit - Hardhat Beetle 2" + - [ 0x00e8, 2, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Super Bunny Exit - Hardhat Beetle 2" - [ 0x00ee, 0, [ "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Sprial Cave Top - Mini Moldorm 1" - [ 0x00ee, 1, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Sprial Cave Top - Mini Moldorm 2" - [ 0x00ee, 2, [ "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Sprial Cave Top - Mini Moldorm 3" From 9a34024d901ad44e56d020173d0cddebe53b8c4d Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 14 Jun 2024 17:06:35 -0600 Subject: [PATCH 19/28] feat: various indicators for fake boots and mirror scroll added --- InitialSram.py | 3 +++ RELEASENOTES.md | 2 +- Rom.py | 2 +- data/base2current.bps | Bin 117396 -> 117528 bytes 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/InitialSram.py b/InitialSram.py index 0bacb744..c5c6a814 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -210,6 +210,9 @@ class InitialSram: equip[0x371] = min(starting_arrow_cap_upgrades, 70) equip[0x343] = min(starting_bombs, equip[0x370]) equip[0x377] = min(starting_arrows, equip[0x371]) + + if not startingstate.has('Magic Mirror', player) and world.doorShuffle[player] != 'vanilla': + equip[0x353] = 1 # Assertion and copy equip to initial_sram_bytes assert equip[:0x340] == [0] * 0x340 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 30ba40f0..2ceda908 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # New Features -None for this release. +File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!) # Patch Notes diff --git a/Rom.py b/Rom.py index 5f2adfb9..93cd5855 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '2cbc343d4d554d38d3b0c9c348ca6601' +RANDOMIZERBASEHASH = 'f38b8d05463222c940e9e8115f2f1ddb' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index b1a1f947ad9281e461a4051ac7df5f4631502e3c..491ba32fab1dc051a8bc1175f9335996ef2af56d 100644 GIT binary patch delta 11031 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 delta 10919 zcmX9^30zah*1vNDVTZ8q5H35igNllZh>GHZyCNzoRWw@n4Hf1_0tAR53@|`|Tp)k} zF-VGNU81K5Nw~Z7tGj(^jjculf!B^2?q7neAk@v&^}vsWz*uGAj=TKoJrG z+7*j4O^u(ZnP5c^17<4jzX0_}54;6ptcEs?^tSej(yW52%qa8==;iy?aRbsO)K0H4 z@w@V5I^y$Q&~JLtZ@gH`<2_t;;MNuyvlm5}#M!Jp!*zSY7$H=!%r|Ix@xuq^rI;AIKjPs^_5!Yq zZO|U`iS+1KsMEX@EY(f4umGSM&9ICIw{!YlhR05*Nrg^E9~bI|6B(ku^6a?diWm1u03k1rsV`x-iA z2>s)p;wiqT4uLr6lS(x8lTX;X0k)80+xOwJDh8uWj})LmO&$vYMn;bXAO}tJjBu*F z!PQhT(dC@M53ee?So+<7v=ZfeMwvb8;JRhZebnnY&h@p9JH-~AlODOwxv_oBMRu7= z%t>dVx1JfG1}*Wbr~kZ;j9wFx)jb@O|E=s4D>bmNMq+*vEg6(X{Z^O&RTf_2Oekf1 zPU5s##TS((nWa+ZY9BnDw$FfD0(km#(`>Uxg>txa`7u8OXZQ$638n)pc zD@%eh<{_tcBV`7;Wt|r*V-CvOyUBhGH%ia34Hwyl6sY3aR-*krF~9+}`$U@UZ0CNE zF$20sKJNk8iM|zffDlyZrvp`JGBXaGM4K5TV4(E3%jz<@HGu;?Oi_MM!X;^i)L9__HhL>4~+zdfjs0Kw2Z#-B@zYA zqF;BQo}e~x6|D}Q?CW@&gC*K{z=2s3U>*$C?U~`R(gfON#I`fNrC@r$c?vtD95$4Mm z*mPQHSp#ztJ&U{`xOS6csLz|P&533c#8sKwb*G|&XmXOiiU|gh=bzy-y|P9%zD zTdW?0qOiCC5Tsiaw~S|Z<8$sp;C@LWC~6WjL|67q_^>8ZM^qoAj>|;%{}u~a78aAE z+=JDpWXx!hdVnpfmof#FEi;$5K49TJZGjVPU^djK$v{VHBup_*Fm02Jj66_eYDr-Y zkv6<(zG9bH#(et;vQ8+Y|Iwkkgh2YI9&|Oq&GlK0`qnpP#cUZ-sqm4E`A4TdCu3}o zF=35Md99lKepxqL_Je}iQmY=J$O%hE8zzl6TUV<#$e46p!z8t7xcHoM!4_FrtsuDE zOk)>3pGfn_ZGEC0drr!jesRsqvg`pXbAl4)zN!`NnC@o%vBU&QOk{E*x@kx;eap1A zTCQ{B9uxv~dyl6V$(R6%I*BZTHBpU|E4h&?3MSCDVV9%TvkF!Rc0-k zmLvr0(E217(1#8r`2%xQm*gQxD3qinR!SCySF%wpxvur-Zqg#K5&6vsjnwMZHLSP> z@$s0z?jnc!nO;5r*0l$$IEkE(iC5H|jETIW&Lo*H^ytuxCk{t%sRfWd@i}`!z&4ci zM>9z%CwVh52X~VF^LL)-eqo_NtYA9tt4F%Y$Vo?1m`)aqz*C)vN$~?2v+Zm3u$CLR z@an;@k24EonNDA;4~bRRt0cCW^>DBUhik}Yr+tfZTjTto*PbumX{0r*On zee9$qKsuY^Od3qlZ7Z1xfW7W?X%-)tBD)HIE0q_^3XqsB?-Zkq3WnaX0_9aK0SD3D zicoM9{axYhq^Q&%{Ua~*19kaey{7^`#A-$B-u z6M+z=RPK!OT|FQj7ZZ+Xb&p%b2}w+(E@AwDstGIVhLnw15e3aA4rnJ&95@lgM!~(4 z&`@O(oi`l?RE2{mw7AMC>O(DOYEw&5!^6sD6Cbg3yO>RC7k4F7q5@$-mP9DY>gsG{ zPm}p~Au*ncDytScPhbYlLpXIvsFlF(LD(IqMaU7otxBF0sT|Cv%6ao;%0bI9{6#qk z#xSiu+Y<8w25g{t!i!u#02=4HyC z5>aVdXDc^oVc-iDYGG(|=Ot)suu6M<7fq1L&^w8!rAs4Gkw(li;b%~u_7oRjFn>~-B z@v;?{s*(u*gH7#aY*D+Lcq!{DZrtSgOjX`uOLkw&VZjm(%UIFeXzk`u1|W^Eng)6QMx2O zzughlE8XqGhi@B#wb|+U1}p9Q^n7nUx~bd&>`{FEf#~4KA9QX9p7fF>eV9=q-;kc) zOmP%;n_QqF%V{cnsfH$lw8~9f1q?irLl5h{T((?1V=!yB=Ee$gM?14x=HHagw?ftp z>xo~tyV#{V-Kaex@Tw__7U!e?JR`LW9{; zYa>s%ylr5Ze6qaT{Mtg>#VwZDc=Y$-Q*Jy^6lsh5dooa z;;%RHrXN{=YS}C-W@#KhjHZvU0LunxQEF?+AkSzj9O!^N;f0Qztd=7!rB_tusIke_ z>7c{IOZm0C>_n;NOim|P&IrHnK!Z&YsX_~gmcEje^AaEe1@Z$wHYqAZV@Q zVt4d$UDr+S_`zPjgy-G^7Yb!RqNC0&P)8+b0D|TIR$T%k&~G_fw%Pe+N3L&i1#C z_+Cbd9+s;7-kgy=pgT079G@|1kapJvxfg24-U}%d)w&jBBU!6kW_C~co7_=OD0;{N zt#t^L4n>bDw+6%7^4G!>JzWp5d+z&Qva!^aW+|kMPljSfF_rti%it&^U2VIpO`cX504G|}HL zdF3>bp>ChN9Jd|aJ|gWkqLv-qb{_d7Z}0=k42O76y+ovyuZO)vX)26igB8Z0L1bj` zj&P)J4+19WdHXgHhZdi7CpJ@bG7juRs*}miM|-*3Jf0y@85;Ul5w9^%?Zo?n(Vr(* z^A|Zld7eF5u89DS=(r|4a9c7|2>ONEM0rKkPduZEJQ2lAg38I0;ElA`c^-0-Ft+HP zCL$@=7vA=6JsHZQ25HLRcq!V9m~w+vh$`iID+D30cs|UbQ5jy>C+Coz1#A%BJR349 zN_~hH_p;N_fhlN4#~o9STKsubm33qtd$Mk+UJ1D}X3bcfHddtS`H!p=6wob{pFr3XFMO-9Cc>hJ%_8<%a1L zl=etyf4DA*A`(%Zo{{1GI(`9xnOoJ1xI!K(^0@$Ipgp{nxoqw_j zO9?Isep8*l=0<~nDA0EUS|i>9tK`NTdD`EVwyfg!8+quyPC*>lEuTc&@`B&UYT&lk z3JR)Lg`w6@GAv_~pUE;$jTR}bkwx!egFSm2|5*PPa(*-|lQYU5^eWzG+#X z_(m%x1$CcFhJ?zElio-idMl21`i8MpS*=t%rPr;9U?u(0b&gPS>vmmR=~?VI?G1Yi z3y`L_(I(iym0KFjwe>!EcLLF}v!Slzo#9nky-!!8&)g3mwo;U=(MJQ21FAeLH1+Hh z-*rdl&w5XHH*lnpw$Uf=S5HDV`oN2%#~t6!DJ<)aLRhv^C011Xc*v?z30tWYm)=y} zDy?a$W&JxN#H=53?#l%8(4IbnPw*Ea?dQa-3*@1Y*b##*Rw7X(E8X!&9@;z~Wu5yL zSfRx85vDHS9S%kxpPxl%<)Pm5Rdap8yWP;45;2-{95cCy?H9Ch`e*mKApuig1Y_=T zvG=&E_qg=?+;{i6r}sILb_VfLff0!Mqnk6`H3su)J6e3}V;h7%ogHVkee`es_EFZ- z3W}Bc#9!?fS5HX#(Gza*tz80_?)`~1{X>-gJZfi^yb1Q;Eh&ajI?;zsLtF|?Eij?uKc1K zpecV{$ECYArYEndnbQbu{*n})tO=_KPrVYYb)HMr)?CRm@^TjgasM9RtN>Z7$_A@Y5^o_1V>;A3}kZLWJfj&@8yw;l`W4fg28;~`Vm zJw;^I4Nvx1O}wnmV)l7`qBcZcQz=L_dJ4UoN{nH7DU7O~w%Q|8Z0DC&n(Hi?(i(2~g|sR?rq z4gYW$tVJ7t+zA5Fmp?`cJ=YgY1fwVhp$!Q?HfZQnXiK~@Rq(O`sfL%TqLJV8U@%`d z_qhg`9rm>J*6+|b<0TOMe^@Dk_H6sT}*-(gyAxs71+Vc!$6h4F)EZ1{K04>CF2RXfrJ=I z>4evLHE8~9$VOu)t>R&;r!gB_uR;e~2#>fb5F#*l~IXGcmQZXB-O8gWpjw1wd z#slSI`Nzj`a%3?Zgm-+(2B`!WP+&aCk;)*sSQ_*?4_n5g)}PndESg*_cmKU^^4v!# zXsyoXmv{i=DEVcGbF_hV`aZj}Z*?f_3}~0hKT6L>J}CdCN9Z^b3v^W-;FU-(aW?Yz zcn)16fp$vj8KPR6!k;vYG*rVC8|7*q>VFw#lfs8GQkaV-!j>5H*Gq3=zFl7l-D2KL zQ-~Z!PbXITg~aIcrEpd|D^e{)i(btJn^5(u7%)*c@M@{8O@2m6ZD(ccfZJ9IXQGgQ zLalaBFOf0TKSJfEohb94<-*Ufx?jL(s?@^K>P+4*uwOukeqpn%i1`7l83*0ne>T%D zi=0k$4GD=xs$#okdwHU2laMO;%&G+-4)Cg2-~^beYVecQoQ*}YJJoDA4%>~30r=K@ z-;uA8-H*y~SVV&eaJ8?N1{lzOt8uIeU_ff$auZMrKt6tE3aTf3r&KdwlfseGYti5hP$ZFbpA&s-*YvO+3~2En0*mri@W$BV0=`v>^w77!ZD=< zTnOpDo#m7vB?^JWOO)E;ybP9J(O;EleLJW1Nwf=<3;K}HHz*tP&A?PJ9jBOqXq&z1 zP_pMWKAiUFKbdxzH3Pmr+fv@zi;dRASGd93MN%=nt6!+;ST29Y+rPm+v+Vx6Ivi#W;-{VN;)wTAO}zF?Q>9SEJn7>yL+^47d5~EQd}*q2lGuw{1%nQT zH`(^0f9veX&~49h=7`4i<{&OE)}yG2f10yvOrk}lcyB;)l~zSOGy%@mw2lQ~$!JI( zuTObo3e8)ij3!J9rY*of!HsDCt1snEO{=I=cZgNY5oES zTY`DC**?725~R@d0es#PEXco9=^*?XX;1KWg(gu(Uu{ywoci-dtLAAH+#`YTbSVsJ zUZ}dqdbhW;6I3;O()b7!q7pxm{89NLkH~=LcJmk9kff4J$s(}U7*b*lj4qAr!yN;}r zE5XfU>(%l?qRmh>DLzKrA$xfVI)}ej{g*wUdLXrE&QmHR+BK?6vO>bIUCo|QT_mN| z_$O;3nx9}#8{n0Hdr+a;YKo*I( zqbL@l|Ki*Nyt+)euB@T?PtwROoR=fE5VN+*g)-FI zL{sFb_#jT$UV+pk561FA$b<`MufTU~fTzobhyqnsM1dmA(L)xFOOVBQnQB#hwJnI_ z`_1`Il|BbgvISmXJYHc7`UDGY7ul{~xZbwRMdniP(%^z4?7*Olm&6nnT57)akhuvw zWF7+TMk0)v*jfN$(u2!nD|*y5s(>Mo;Hh(dVEhg z-KS~+We{nLplZg!vd8PqhoT>5FbB?(SVhR#m1LpjV{h3BN9=UCtjbZd#amX6t())) zdk_tzxX2#-VCxHxv$5Zc^*PjWr5}Y4JAf#G2RJTiHlY3!lUP=C%a_LI96$*D8OB2n zAezo`#!imF&%)XAv6KrJ-~c?$5yX4!t*)gFmfFVje5GqQ) zyC16kxl`2TU?re6R)f~$wnxbvtVF7bDR`+9=mPZ^I0JWnZCz<9PR2rKFvT(a;0e}5 zDGr4E#rYbVl9(`0@yE_!iS3jyx#Z$VT|R0kna9}UFPuTJ$&m)k;J=)KSL~5`mD^z6 z`D67e4+8o1s#OHodR4|?UT$@W<@45or|#v0Lqg&7dK`K9gxvizJjVq@WQCa6K~=Y& z(^F!Vy;vn+eW4$#FISyWaV-KdDrZs4bTP@7s|+dtRpotI&=l%3uJW(WzRSNmyWjGf z<(HOUSzhN~pM8&iZ}!f4J1z4p^DP@!6-Y|F@r)FF#|5kh={U+2xaRMg|47=LXnL(y zaoJQ;ZlbEJ-rggbe?tDU(L$#jx4o%--u9-JQ>c}_sQZBr|D*I@DU%q2T;O<{+jOzl z!A8~IIU{Ym=SyWYO@{_a%oj+p<&2Sb-;5VhJ^Wo6F!v`m?C&Y_%h9;Q-=mFY?^sdu z;@S1ewMi_iLF)_C*qJR!F07P>t`8UUs$1OF%Nk6zMZf2XG(zR3b+WEm56#EolO>@^ z#)&xQ`?ZDFn$$O#Zg2gKF)Vl1-y$gNw$TqZVS6|56Md-#kGg@FWfz;_qp%in%hDEA zi|I9wr6(HxZ05JvwHTXkHEAvk3>>AX%M>**pyKV4op)1a{hNWNA3ikOw5W2o%4R{J z%xX0cg_MCew&`@1&>QEugAlM6x4VO+{JNR`rA=yz`j$Mh*3-T=QpI2^Mcq~If$TRr zIs2PV*=Ltk#O_mq<;}`1SzXWCw`(FhzU-u^hvcECdm4%wCDbEL4;}ir;nc2E+$oAW zsY~4{Ra__3=gOpzJM2Be>X#mWr}m%LTA9K@`_t~H$2$8t5D5L3+=92|{?&G10>t?q zU@6bp^dY|Q0WQ!EK7AFQARB-!_^lTRHTS`1$XvL!eIeeU%mk$1UUH?T-<`tK8PGtF zcHt)s*y1V5mfhu1ns^fY);Q4e%Tw~Q?4TXpGmgktmOohP7ZC_oLvt=BpgA@G^1EjK zA@t}e%eZwS^H#sGYvUin1bojQ_|qx=_%DA@20Ub=&gIy z9s26aB}UtRA&K&IW#a=2uw4-Gpgi!@Ah44_R}dKIy7@oqdE{eJeo`)k--Oy(ly_t? zw+HdBLBQS2qo-deBBQhm2E|tH6D6H{CP-|cuP<&628(@`FDk~%78SFzE{HWLyNqT> z#(WxRi+^U5(EcE79|9OU(G8Ce0k7x#E_`y{rB@x*w~ASwEvS!N2Wb7qkuP!_KM&;|7feljrtu*5Gd> zVm#qIj34}+5TW>+P%zQ=+jGM5bJbHPHK`8Fy2lzDnRzYjZ;g=sy%B!%2Xg>Ng@JI# zfWH3-UaNd3Nmv68no+YFlHMPOfeHNBokjBL-{FohaK_o^dkvUL%mC}B5y-^ynjeq* zX-x61aUe+;a!&2GwIJ(fIc-n14pz&$QpXBfHvBAiu&1gc3-MRufNs*>dUYGGfiGyI z^!rh+gv?0H9wb)=TY`s?xK$vy@SkWJemWG5kLBO1Z5{9kwUZ2}Jg7c>IM_@(rtRap zD0&0=0|7-7Y*pqR!U<8}Dj;GW1zMBr^#8t=Sfp2dlv~qi{ZW}Bio8wG>m6*w6^XmQ zZMToU#=dKD@_Tr~VFy%3gcVAiwI}gf*CLrIWwcN&#NR~|eJ{XoqQUIhnODmB9v&W{ z?>|TehR7lLpZ;V)wD1*4V?+;2+_nm-4vrGO*N#96Qro{@!v`Cneu{M|38 zr;_XDdKKfYI1p#!xz7w%FMTiR7u?#m2fvIXj>RUC04`_8Lag2GLxPT{W`_0TFGScU z9?Y@XxJ3Y~D5Fi+K_4^)?};ZDk1$*n4+e-yUN;`>0y2DOJU9mC-~|)F3a_S{zr+FK z9~$cCt|LB1tJ}Fg<@|(~xjyiXa1Pc_0HO4b4R~Y%Nb`ta^=sF6(;svj`FfJVKa@VQ zK{Di2rUoyX2(EX{>-`7Y}e! z^n6wJ(a&q;{Pg^vL+$Y4T)b`)2oz3El&CTuS1by=fC{Tf=+7yc>g;7r$%`=(ZNz&s zaLXi+ZaZ)Gx8#H#Cjn`;^P{PKU@~Ym15h;)5)X9S%sP5^Q1X#bhl1O#i>;Ly%&%+C6OW@kdk2@1D?kAcKGC)CW+QuMH9>=nuTuhmlGhH6YzUaSNfzYp04U5io@ikb|T06rxThvQuF^AylQTQ0#B zvp^&5>VsovgJ{0f!SV|pd-`%_gKIQ>bQO-63+gS)P9M7f z907FJ5&V83NCWfw<}Ctp04(|*rh|pRbd9i9ISEHB0i9rG-#1HunzoPgsB7)+Hfgqi zbhBj%=DYWuSPoVLI$$k+u>!=>84lQcB?uGP@0PY+se#5o@}B|i9_))(t^|p3N1YU^ z-8Q#RtUhzv>kalz_*CIUSyE2Q?Ne%deJEyEC8wPQyF))1fHGkDTZs|infT^PkP33K z-72uh+BC0DVdM*KYdW21EL#OG(uZc@wACOC?7;_CgA}m2@4{-}2W&oauC20YwHy>s z*nAp(kpVs?stvDR0|MzIVHmCjWQtr3p}_6 zggfnyw1v{;)SBw>xuq3{2iT3u_~mze1&u(E5;WW#$?ca-4|YeFP%u*r>jc z%|HymMclasEVEmiTCB*KEaJG+>!Qo?K5UyqBp^K(Kgj`+z!BSQ1yNubp1Kvp3aoY) zS3R$IUeTF@;!5zAEjWKG*kENAtO)-|>%Xi%+^O&9t)Rx1Z<^TgYtF(x=^^ljM_buq zcocZh%Y=RPM}aS}Kb%s!r&I6JrGglAYV ztDkl3akAC^y2HI)vN)zQ)3?1(T@E6{==iz#-lrfi;ArCYu8^EkqL-IsKMtOC6~1@F2<9OdbKhD$(iV!39y zw8EDB6Kbx1Uu{3A19nM4Z_ZX$QAs4!S*)}MeaY9sS)Tn${*Bft)G;cHRCrOk-I;wB S1Hg=De&*_&(0_iT_5TMlj;1aE From e6b0239d2449cbac06d671ea4b6471176cfd5fcd Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 17 Jun 2024 17:16:22 -0600 Subject: [PATCH 20/28] fix: customizer bug when placing small keys fix: hera basement cage fix - again --- DoorShuffle.py | 2 +- RELEASENOTES.md | 4 +++- Rom.py | 2 +- data/base2current.bps | Bin 117528 -> 117480 bytes 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index e6fd8baa..14433143 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2087,7 +2087,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_ if actual_chest_keys == 0: dungeon.small_keys = [] else: - dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player)] * actual_chest_keys + dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(actual_chest_keys)] for name, small_list in small_map.items(): used_doors.update(flatten_pair_list(small_list)) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2ceda908..c8a6ce5f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcod # Patch Notes * 1.4.3 - * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues + * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues + * Hera Basement Cage: Fix for small key counting multiple times (again) * Generation: Fixed several generation problems with ER and intensity 3 + * Customizer: Generation bug when attempting to place small keys * Enemizer: Various enemy bans \ No newline at end of file diff --git a/Rom.py b/Rom.py index 93cd5855..249ca8c3 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'f38b8d05463222c940e9e8115f2f1ddb' +RANDOMIZERBASEHASH = '5f039a5d88474aefc66c116beb67fa68' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 491ba32fab1dc051a8bc1175f9335996ef2af56d..05c1fd24b5c0eba3dad03a9aa19f3d6582f55981 100644 GIT binary patch delta 350 zcmV-k0iphwmIvsS2e1MI1f7^-XOjj4f&%xqvx);j2LS`K%nJAt4{f)os~u#Fjf#V| z>WGP+v(p`@1_3j(d?cO$0ZFqPCQ$@#A6ycYoXaRmTS-7a1tBah$jV1!( zF#2l%L?A@KL{LuPPNOBz!U63F2tlg&;HSTynW;mZTmS$7 delta 399 zcmV;A0dW54l?RxX2e1MI1WSRmev<|Rf&wSFvx);j2LUs)%nJAt51qHDs~u#Fjf#V| z>WGQXv(p`@1_5KUd?cO$0eQ0F^Iqs+iq7x6lLnIAF;n@cH@}V7!11z%tMcqJ1ln(TDmNYdCAYAP&iw3}^vM z9-6nSU?8-&B3<^KFac|&4ao@5E3I+^&>@$99sz)tbZ7y7HHo;Wo2!eQ!Hp&Y;xPJa z07M`}z(i0^;7+3&VxO~9XO)2!w}P3%AgNKgfD^ZbvBDs!0lAkaX#qJx07l<|Oq+s% zT$_S{<(q Date: Thu, 20 Jun 2024 16:20:44 -0600 Subject: [PATCH 21/28] feat: minor stats compilation utility --- test/stats/SpoilerStats.py | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/stats/SpoilerStats.py diff --git a/test/stats/SpoilerStats.py b/test/stats/SpoilerStats.py new file mode 100644 index 00000000..8e166c60 --- /dev/null +++ b/test/stats/SpoilerStats.py @@ -0,0 +1,52 @@ +import os +import csv +import collections + + +def process_file(file_path): + items_at_locations = collections.defaultdict(list) + with open(file_path, 'r') as file: + location_section = False + for line in file: + line = line.strip() + if 'Locations:' in line: + location_section = True + elif 'Shops:' in line: + location_section = False + elif location_section and line: + location, item = line.split(":") + if 'Bottle' in item: + item = 'Bottle' + items_at_locations[location].append(item.strip()) + return items_at_locations + + +def process_directory(directory_path): + all_items_at_locations = collections.defaultdict(list) + for file_name in os.listdir(directory_path): + file_path = os.path.join(directory_path, file_name) + items_at_locations = process_file(file_path) + for location, items in items_at_locations.items(): + all_items_at_locations[location].extend(items) + return all_items_at_locations + + +def write_to_csv(items_at_locations, csv_file_path): + # Get a list of all unique items across all locations + all_items = set(item for items in items_at_locations.values() for item in items) + + with open(csv_file_path, 'w', newline='') as file: + writer = csv.writer(file) + # Write the header row + writer.writerow(['Location'] + sorted(list(all_items))) + # Write a row for each location + for location in sorted(items_at_locations.keys()): + item_counts = collections.Counter(items_at_locations[location]) + # Write a column for each item + row = [location] + [item_counts.get(item, 0) for item in sorted(all_items)] + writer.writerow(row) + + +if __name__ == '__main__': + items_at_locations = process_directory(os.path.join('..', '..', 'analysis2')) + write_to_csv(items_at_locations, os.path.join('..', '..', 'analysis2', 'output.csv')) \ No newline at end of file From 9ecd88341f380e47ac12e4140499c7a29954e4dc Mon Sep 17 00:00:00 2001 From: Cody Bailey Date: Sun, 30 Jul 2023 16:17:40 -0600 Subject: [PATCH 22/28] Make item hint text less ambiguous Update Items.py Fix some typos, update hookshot text --- Items.py | 234 +++++++++++++++++++++++++++---------------------------- Text.py | 60 +++++++------- 2 files changed, 147 insertions(+), 147 deletions(-) diff --git a/Items.py b/Items.py index 8a55f2f1..76dd7bfc 100644 --- a/Items.py +++ b/Items.py @@ -20,52 +20,52 @@ def ItemFactory(items, player): # Format: Name: (Advancement, Priority, Type, ItemCode, BasePrice, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) -item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the bow'), - 'Progressive Bow': (True, False, None, 0x64, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a bow'), +item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class!', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the bow'), + 'Progressive Bow': (True, False, None, 0x64, 150, 'Bow!\nJoin the archer class!', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a bow'), 'Progressive Bow (Alt)': (True, False, None, 0x65, 150, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a bow'), 'Book of Mudora': (True, False, None, 0x1D, 150, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the book'), 'Hammer': (True, False, None, 0x09, 250, 'Stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'), - 'Hookshot': (True, False, None, 0x0A, 250, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), - 'Magic Mirror': (True, False, None, 0x1A, 250, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the mirror'), - 'Ocarina': (True, False, None, 0x14, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), - 'Ocarina (Activated)': (True, False, None, 0x4A, 250, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), - 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the boots'), - 'Power Glove': (True, False, None, 0x1B, 100, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), - 'Cape': (True, False, None, 0x19, 50, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'), - 'Mushroom': (True, False, None, 0x29, 50, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the mushroom'), - 'Shovel': (True, False, None, 0x13, 50, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the shovel'), - 'Lamp': (True, False, None, 0x12, 150, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the lamp'), - 'Magic Powder': (True, False, None, 0x0D, 50, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the powder'), - 'Moon Pearl': (True, False, None, 0x1F, 200, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), - 'Cane of Somaria': (True, False, None, 0x15, 250, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red cane'), - 'Fire Rod': (True, False, None, 0x07, 250, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), - 'Flippers': (True, False, None, 0x1E, 250, 'Fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), - 'Ice Rod': (True, False, None, 0x08, 250, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), - 'Titans Mitts': (True, False, None, 0x1C, 200, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), - 'Bombos': (True, False, None, 0x0F, 100, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), - 'Ether': (True, False, None, 0x10, 100, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), - 'Quake': (True, False, None, 0x11, 100, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), - 'Bottle': (True, False, None, 0x16, 50, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'), - 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'), - 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'), - 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'), - 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Save me and I will revive you', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'), - 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'), - 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'), - 'Master Sword': (True, False, 'Sword', 0x50, 100, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), - 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), - 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the small sword'), - 'Sword and Shield': (True, False, 'Sword', 0x00, 'An uncle\nsword rests\nhere!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), - 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), - 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'A better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a sword'), - 'Progressive Glove': (True, False, None, 0x61, 150, 'A way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a glove'), - 'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the silver arrows'), + 'Hookshot': (True, False, None, 0x0A, 250, 'Hookshot!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'), + 'Magic Mirror': (True, False, None, 0x1A, 250, 'Mirror!\nTake some time to\nreflect on this moment!', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the mirror'), + 'Ocarina': (True, False, None, 0x14, 250, 'Ocarina!\nA Flute by another name', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), + 'Ocarina (Activated)': (True, False, None, 0x4A, 250, 'Ocarina!\nA Flute by another name', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'ocarina boy plays again', 'the flute'), + 'Pegasus Boots': (True, False, None, 0x4B, 250, 'Pegasus Boots!\nGotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the boots'), + 'Power Glove': (True, False, None, 0x1B, 100, 'Glove!\nNow you can\nlift weak stuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'), + 'Cape': (True, False, None, 0x19, 50, 'Cape!\nInvisbility cloak activate!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'), + 'Mushroom': (True, False, None, 0x29, 50, 'Mushroom!\nDon\'t eat it. Find a witch.', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the mushroom'), + 'Shovel': (True, False, None, 0x13, 50, 'Shovel!\nCan you dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the shovel'), + 'Lamp': (True, False, None, 0x12, 150, 'Lamp!\nYou can see in the dark,\nand light torches.', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the lamp'), + 'Magic Powder': (True, False, None, 0x0D, 50, 'Powder!\nSprinkle it on a dancing pickle!', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the powder'), + 'Moon Pearl': (True, False, None, 0x1F, 200, 'Moon Pearl!\nRabbit Be Gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'), + 'Cane of Somaria': (True, False, None, 0x15, 250, 'Cane of Somaria!\nMake blocks, throw blocks', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red cane'), + 'Fire Rod': (True, False, None, 0x07, 250, 'Fire Rod!\nI\'m burning for you!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'), + 'Flippers': (True, False, None, 0x1E, 250, 'Flippers!\nFancy a swim!', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'), + 'Ice Rod': (True, False, None, 0x08, 250, 'Ice Rod!\nActivate Freeze Ray!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'), + 'Titans Mitts': (True, False, None, 0x1C, 200, 'Mitts!\nLift ALL the rocks!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'), + 'Bombos': (True, False, None, 0x0F, 100, 'Bombos!\nExplosions! Fire!\nBurn it all!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'), + 'Ether': (True, False, None, 0x10, 100, 'Ether!\nLet\'s cool things down!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'), + 'Quake': (True, False, None, 0x11, 100, 'Quake!\nLet\'s shake the ground!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'), + 'Bottle': (True, False, None, 0x16, 50, 'Bottle!\nStore all manner of things!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'), + 'Bottle (Red Potion)': (True, False, None, 0x2B, 70, 'Red Potion!\nFill your hearts!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'), + 'Bottle (Green Potion)': (True, False, None, 0x2C, 60, 'Green Potion!\nRestore your magic!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'), + 'Bottle (Blue Potion)': (True, False, None, 0x2D, 80, 'Blue Potion!\nHeal and restore!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'), + 'Bottle (Fairy)': (True, False, None, 0x3D, 70, 'Fairy Bottle\nI\'ll save your life!', 'and the captive', 'the tingle kid', 'hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'), + 'Bottle (Bee)': (True, False, None, 0x3C, 50, 'Bee Bottle\nI\'ll sting your enemies a bit!', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'), + 'Bottle (Good Bee)': (True, False, None, 0x48, 60, 'Good Bee Bottle\nI\'ll sting your enemies\na whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'), + 'Master Sword': (True, False, 'Sword', 0x50, 100, 'Master Sword!\nEvil\'s bane!', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'), + 'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'Tempered Sword!\nMore slashy!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'), + 'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'Fighter Sword!\nStarter level slashy!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the small sword'), + 'Sword and Shield': (True, False, 'Sword', 0x00, 'Sword and Shield!\nUncle sword ahoy!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'), + 'Golden Sword': (True, False, 'Sword', 0x03, 200, 'Golden Sword!\nBest slashy!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'), + 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'Sword!\nA better sword for your time!', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a sword'), + 'Progressive Glove': (True, False, None, 0x61, 150, 'Glove!\nLift more than you can now!', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a glove'), + 'Silver Arrows': (True, False, None, 0x58, 100, 'Silver Arrows!\nDefeat Ganon faster!', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the silver arrows'), 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x37, 0x08], 999, None, None, None, None, None, None, None), 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x39, 0x09], 999, None, None, None, None, None, None, None), 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x38, 0x0a], 999, None, None, None, None, None, None, None), - 'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), - 'Power Star': (True, False, None, 0x6B, 100, 'A small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), - 'Triforce Piece': (True, False, None, 0x6C, 100, 'A small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'), + 'Triforce': (True, False, None, 0x6A, 777, 'Triforce\nGrab me to win!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), + 'Power Star': (True, False, None, 0x6B, 100, 'Power Star\nA small step\ntowards victory!', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), + 'Triforce Piece': (True, False, None, 0x6C, 100, 'Triforce Piece\nA small step\ntowards victory!', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'), 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x20, 0x01], 999, None, None, None, None, None, None, None), 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x20, 0x02], 999, None, None, None, None, None, None, None), 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x20, 0x03], 999, None, None, None, None, None, None, None), @@ -73,98 +73,98 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x20, 0x05], 999, None, None, None, None, None, None, None), 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x20, 0x06], 999, None, None, None, None, None, None, None), 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x20, 0x07], 999, None, None, None, None, None, None, None), - 'Single Arrow': (False, False, None, 0x43, 3, 'A lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), - 'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), - 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Single Bomb': (False, False, None, 0x27, 5, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), - 'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), - 'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), - 'Big Magic': (False, False, None, 0xD4, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), + 'Single Arrow': (False, False, None, 0x43, 3, 'Single Arrow!\nDestiny awaits!', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), + 'Arrows (10)': (False, False, None, 0x44, 30, '10 Arrows\nAn archer\'s bailout!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), + 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, '10 Arrow Upgrade\nGain 10 more arrow capacity!', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Arrow Upgrade (+5)': (False, False, None, 0x53, 100, '5 Arrow Upgrade\nGain 5 more arrow capacity!', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), + 'Single Bomb': (False, False, None, 0x27, 5, '1 Bomb\nBoom boom pow!', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), + 'Arrows (5)': (False, False, None, 0x5A, 15, '5 Arrows\nAn archer\'s bailout!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'), + 'Small Magic': (False, False, None, 0x45, 5, 'Magic Decanter!\nJust a little one', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'), + 'Big Magic': (False, False, None, 0xD4, 40, 'Magic Decanter!\nFull refill', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'), 'Chicken': (False, False, None, 0xD3, 5, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'), - 'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), - 'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), - 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), - 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), - 'Blue Mail': (False, True, None, 0x22, 50, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the blue mail'), - 'Red Mail': (False, True, None, 0x23, 100, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the red mail'), - 'Progressive Armor': (False, True, None, 0x60, 50, 'Time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), - 'Blue Boomerang': (True, False, None, 0x0C, 50, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the blue boomerang'), - 'Red Boomerang': (True, False, None, 0x2A, 50, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the red boomerang'), - 'Blue Shield': (False, True, None, 0x04, 50, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a blue shield'), - 'Red Shield': (False, True, None, 0x05, 500, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'a red shield'), - 'Mirror Shield': (True, False, None, 0x06, 200, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), - 'Progressive Shield': (True, False, None, 0x5F, 50, 'Have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), - 'Bug Catching Net': (True, False, None, 0x21, 50, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the bug net'), - 'Cane of Byrna': (True, False, None, 0x18, 50, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the blue Cane'), - 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), - 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), - 'Piece of Heart': (False, False, None, 0x17, 10, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'), - 'Rupee (1)': (False, False, None, 0x34, 0, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a green rupee'), - 'Rupees (5)': (False, False, None, 0x35, 2, 'Just pocket\nchange. Move\nright along.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'), - 'Rupees (20)': (False, False, None, 0x36, 10, 'Just couch\ncash. Move\nright along.', 'and the couch cash', 'the piggy-bank kid', 'life lesson for sale', 'the witch buying drugs', 'destitute boy has lunch again', 'a red rupee'), - 'Rupees (50)': (False, False, None, 0x41, 25, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'), - 'Rupees (100)': (False, False, None, 0x40, 50, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'), - 'Rupees (300)': (False, False, None, 0x46, 150, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'), - 'Rupoor': (False, False, None, 0x59, 0, 'A debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), - 'Red Clock': (False, True, None, 0x5B, 0, 'A waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), - 'Blue Clock': (False, True, None, 0x5C, 50, 'A bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), - 'Green Clock': (False, True, None, 0x5D, 200, 'A lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), + 'Bombs (3)': (False, False, None, 0x28, 15, '3 Bombs\nExplosions!\nIn a handy 3-pack', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), + 'Bombs (10)': (False, False, None, 0x31, 50, '10 Bombs\nExplosions!\nIn a thrifty 10-pack', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'), + 'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, '10 Bomb Upgrade\nGain 10 more bomb capacity!', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Bomb Upgrade (+5)': (False, False, None, 0x51, 100, '5 Bomb Upgrade\nGain 5 more bomb capacity!', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'), + 'Blue Mail': (False, True, None, 0x22, 50, 'Blue Mail!\nLess damage activate!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the blue mail'), + 'Red Mail': (False, True, None, 0x23, 100, 'Red Mail!\nEven less damage!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the red mail'), + 'Progressive Armor': (False, True, None, 0x60, 50, 'Upgrade Mail!\nNot the kind you read!', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'), + 'Blue Boomerang': (True, False, None, 0x0C, 50, 'Blue Boomerang!\nAlways comes back!', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the blue boomerang'), + 'Red Boomerang': (True, False, None, 0x2A, 50, 'Red Boomerang!\nAlways comes back!', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the red boomerang'), + 'Blue Shield': (False, True, None, 0x04, 50, 'Blue Shield\nBlock rocks!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a blue shield'), + 'Red Shield': (False, True, None, 0x05, 500, 'Red Shield\nBlock fireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'a red shield'), + 'Mirror Shield': (True, False, None, 0x06, 200, 'Mirror Shield\nBlock lasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'), + 'Progressive Shield': (True, False, None, 0x5F, 50, 'Shield\nA better shield for your time!', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'), + 'Bug Catching Net': (True, False, None, 0x21, 50, 'Bug Net!\nCatch all manner\nof things!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the bug net'), + 'Cane of Byrna': (True, False, None, 0x18, 50, 'Cane of Byrna!\nSwirly protection!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the blue Cane'), + 'Boss Heart Container': (False, True, None, 0x3E, 40, 'Heart Container!\nHealth Increased!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), + 'Sanctuary Heart Container': (False, True, None, 0x3F, 50, 'Heart Container!\nHealth Increased!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'), + 'Piece of Heart': (False, False, None, 0x17, 10, 'Heart Piece!\nOne step closer\nto more health!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'), + 'Rupee (1)': (False, False, None, 0x34, 0, 'Rupees!\nJust pocket\nchange.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a green rupee'), + 'Rupees (5)': (False, False, None, 0x35, 2, 'Rupees!\nJust pocket\nchange.', 'the pocket change', 'poverty-struck kid', 'life lesson for sale', 'buying cheap drugs', 'destitute boy has snack again', 'a blue rupee'), + 'Rupees (20)': (False, False, None, 0x36, 10, 'Rupees!\nJust couch\ncash.', 'and the couch cash', 'the piggy-bank kid', 'life lesson for sale', 'the witch buying drugs', 'destitute boy has lunch again', 'a red rupee'), + 'Rupees (50)': (False, False, None, 0x41, 25, 'Rupees!\nA big pile!', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'), + 'Rupees (100)': (False, False, None, 0x40, 50, 'Rupees!\nA big stash!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'), + 'Rupees (300)': (False, False, None, 0x46, 150, 'Rupees!\nA big hoard!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'), + 'Rupoor': (False, False, None, 0x59, 0, 'Rupoor!\nI\'ll take your rupees!', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'), + 'Red Clock': (False, True, None, 0x5B, 0, 'Red Clock!\nA waste of time.', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'), + 'Blue Clock': (False, True, None, 0x5C, 50, 'Blue Clock!\nA bit of time!', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'), + 'Green Clock': (False, True, None, 0x5D, 200, 'Green Clock!\nTons of time!', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'), 'Single RNG': (False, True, None, 0x62, 300, 'Something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'), 'Multi RNG': (False, True, None, 0x63, 100, 'Something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), - 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance - 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance - 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'), - 'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 60, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'), - 'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 10, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'), + 'Magic Upgrade (1/2)': (True, False, None, 0x4E, 50, 'Half Magic!\nDouble your magic power!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance + 'Magic Upgrade (1/4)': (True, False, None, 0x4F, 100, 'Quarter Magic!\nMagic is basically free now!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance + 'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 40, 'A small key for\nEastern Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'), + 'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 60, 'A big key for\nEastern Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'), + 'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 10, 'A compass for\nEastern Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'), 'Map (Eastern Palace)': (False, True, 'Map', 0x7D, 20, '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 Eastern Palace'), - 'Small Key (Desert Palace)': (False, False, 'SmallKey', 0xA3, 40, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'), - 'Big Key (Desert Palace)': (False, False, 'BigKey', 0x9C, 60, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'), - 'Compass (Desert Palace)': (False, True, 'Compass', 0x8C, 10, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Desert Palace'), + 'Small Key (Desert Palace)': (False, False, 'SmallKey', 0xA3, 40, 'A small key for\nDesert Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'), + 'Big Key (Desert Palace)': (False, False, 'BigKey', 0x9C, 60, 'A big key for\nDesert Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'), + 'Compass (Desert Palace)': (False, True, 'Compass', 0x8C, 10, 'A compass for\nDesert Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Desert Palace'), 'Map (Desert Palace)': (False, True, 'Map', 0x7C, 20, '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 Desert Palace'), - 'Small Key (Tower of Hera)': (False, False, 'SmallKey', 0xAA, 40, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'), - 'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 60, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'), - 'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 10, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'), + 'Small Key (Tower of Hera)': (False, False, 'SmallKey', 0xAA, 40, 'A small key for\nTower of Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'), + 'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 60, 'A big key for\nTower of Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'), + 'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 10, 'A compass for\nTower of Hera', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'), 'Map (Tower of Hera)': (False, True, 'Map', 0x75, 20, '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 Tower of Hera'), - 'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 40, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'), - 'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 60, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'), - 'Compass (Escape)': (False, True, 'Compass', 0x8F, 10, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Hyrule Castle'), + 'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 40, 'A small key for\nHyrule Castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'), + 'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 60, 'A big key for\nHyrule Castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'), + 'Compass (Escape)': (False, True, 'Compass', 0x8F, 10, 'A compass for\nHyrule Castle', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Hyrule Castle'), 'Map (Escape)': (False, True, 'Map', 0x7F, 10, '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 Hyrule Castle'), - 'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 40, '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, 60, '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, 10, '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'), + 'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 40, 'A small key for\nAgahnim\'s Tower', '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, 60, 'A big key for\nAgahnim\'s Tower', '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, 10, 'A compass for\nAgahnim\'s Tower', '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, 10, '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, 40, '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, 60, '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, 10, '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'), + 'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nDark Palace', '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, 60, 'A big key for\nDark Palace', '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, 10, 'A compass for\nDark Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), 'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, '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 Palace of Darkness'), - 'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'), - 'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'), - 'Compass (Thieves Town)': (False, True, 'Compass', 0x84, 10, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Thieves\' Town'), + 'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key for\nThieves Town', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'), + 'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key for\nThieves Town', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'), + 'Compass (Thieves Town)': (False, True, 'Compass', 0x84, 10, 'A compass for\nThieves Town', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Thieves\' Town'), 'Map (Thieves Town)': (False, True, 'Map', 0x74, 20, '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 Thieves\' Town'), - 'Small Key (Skull Woods)': (False, False, 'SmallKey', 0xA8, 40, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'), - 'Big Key (Skull Woods)': (False, False, 'BigKey', 0x97, 60, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'), - 'Compass (Skull Woods)': (False, True, 'Compass', 0x87, 10, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Skull Woods'), + 'Small Key (Skull Woods)': (False, False, 'SmallKey', 0xA8, 40, 'A small key for\nSkull Woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'), + 'Big Key (Skull Woods)': (False, False, 'BigKey', 0x97, 60, 'A big key for\nSkull Woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'), + 'Compass (Skull Woods)': (False, True, 'Compass', 0x87, 10, 'A compass for\nSkull Woods', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Skull Woods'), 'Map (Skull Woods)': (False, True, 'Map', 0x77, 20, '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 Skull Woods'), - 'Small Key (Swamp Palace)': (False, False, 'SmallKey', 0xA5, 40, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'), - 'Big Key (Swamp Palace)': (False, False, 'BigKey', 0x9A, 60, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'), - 'Compass (Swamp Palace)': (False, True, 'Compass', 0x8A, 10, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Swamp Palace'), + 'Small Key (Swamp Palace)': (False, False, 'SmallKey', 0xA5, 40, 'A small key for\nSwamp Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'), + 'Big Key (Swamp Palace)': (False, False, 'BigKey', 0x9A, 60, 'A big key for\nSwamp Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'), + 'Compass (Swamp Palace)': (False, True, 'Compass', 0x8A, 10, 'A compass for\nSwamp Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Swamp Palace'), 'Map (Swamp Palace)': (False, True, 'Map', 0x7A, 20, '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 Swamp Palace'), - 'Small Key (Ice Palace)': (False, False, 'SmallKey', 0xA9, 40, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'), - 'Big Key (Ice Palace)': (False, False, 'BigKey', 0x96, 60, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'), - 'Compass (Ice Palace)': (False, True, 'Compass', 0x86, 10, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ice Palace'), + 'Small Key (Ice Palace)': (False, False, 'SmallKey', 0xA9, 40, 'A small key for\nIce Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'), + 'Big Key (Ice Palace)': (False, False, 'BigKey', 0x96, 60, 'A big key for\nIce Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'), + 'Compass (Ice Palace)': (False, True, 'Compass', 0x86, 10, 'A compass for\nIce Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ice Palace'), 'Map (Ice Palace)': (False, True, 'Map', 0x76, 20, '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 Ice Palace'), - 'Small Key (Misery Mire)': (False, False, 'SmallKey', 0xA7, 40, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'), - 'Big Key (Misery Mire)': (False, False, 'BigKey', 0x98, 60, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'), - 'Compass (Misery Mire)': (False, True, 'Compass', 0x88, 10, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Misery Mire'), + 'Small Key (Misery Mire)': (False, False, 'SmallKey', 0xA7, 40, 'A small key for\nMisery Mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'), + 'Big Key (Misery Mire)': (False, False, 'BigKey', 0x98, 60, 'A big key for\nMisery Mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'), + 'Compass (Misery Mire)': (False, True, 'Compass', 0x88, 10, 'A compass for\nMisery Mire', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Misery Mire'), 'Map (Misery Mire)': (False, True, 'Map', 0x78, 20, '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 Misery Mire'), - 'Small Key (Turtle Rock)': (False, False, 'SmallKey', 0xAC, 40, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'), - 'Big Key (Turtle Rock)': (False, False, 'BigKey', 0x93, 60, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'), - 'Compass (Turtle Rock)': (False, True, 'Compass', 0x83, 10, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Turtle Rock'), + 'Small Key (Turtle Rock)': (False, False, 'SmallKey', 0xAC, 40, 'A small key for\nTurtle Rock', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'), + 'Big Key (Turtle Rock)': (False, False, 'BigKey', 0x93, 60, 'A big key for\nTurtle Rock', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'), + 'Compass (Turtle Rock)': (False, True, 'Compass', 0x83, 10, 'A compass for\nTurtle Rock', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Turtle Rock'), 'Map (Turtle Rock)': (False, True, 'Map', 0x73, 20, '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 Turtle Rock'), - 'Small Key (Ganons Tower)': (False, False, 'SmallKey', 0xAD, 40, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'), - 'Big Key (Ganons Tower)': (False, False, 'BigKey', 0x92, 60, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'), - 'Compass (Ganons Tower)': (False, True, 'Compass', 0x82, 10, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ganon\'s Tower'), + 'Small Key (Ganons Tower)': (False, False, 'SmallKey', 0xAD, 40, 'A small key for\nGanon\'s Tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'), + 'Big Key (Ganons Tower)': (False, False, 'BigKey', 0x92, 60, 'A big key for\nGanon\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'), + 'Compass (Ganons Tower)': (False, True, 'Compass', 0x82, 10, 'A compass for\nGanon\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ganon\'s Tower'), 'Map (Ganons Tower)': (False, True, 'Map', 0x72, 10, '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 Ganon\'s Tower'), 'Small Key (Universal)': (False, True, None, 0xAF, 100, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'), 'Nothing': (False, False, None, 0x5A, 1, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'), diff --git a/Text.py b/Text.py index 052defaf..3ca193df 100644 --- a/Text.py +++ b/Text.py @@ -1666,50 +1666,50 @@ class TextTable(object): text['potion_shop_no_empty_bottles'] = CompressedTextMapper.convert("Whoa, bucko!\nNo empty bottles.") text['item_get_lamp'] = CompressedTextMapper.convert("Lamp! You can see in the dark, and light torches.") text['item_get_boomerang'] = CompressedTextMapper.convert("Boomerang! Press START to select it.") - text['item_get_bow'] = CompressedTextMapper.convert("You're in bow mode now!") - text['item_get_shovel'] = CompressedTextMapper.convert("This is my new mop. My friend George, he gave me this mop. It's a pretty good mop. It's not as good as my old mop. I miss my old mop. But it's still a good mop.") - text['item_get_magic_cape'] = CompressedTextMapper.convert("Finally! we get to play Invisble Man!") - text['item_get_powder'] = CompressedTextMapper.convert("It's the powder. Let's cause some mischief!") - text['item_get_flippers'] = CompressedTextMapper.convert("Splish! Splash! Let's go take a bath!") - text['item_get_power_gloves'] = CompressedTextMapper.convert("Feel the power! You can now lift light rocks! Rock on!") + text['item_get_bow'] = CompressedTextMapper.convert("Bow! Join the archer class!") + text['item_get_shovel'] = CompressedTextMapper.convert("Shovel! Can you dig it?") + text['item_get_magic_cape'] = CompressedTextMapper.convert("Cape! Invisbility cloak activate!") + text['item_get_powder'] = CompressedTextMapper.convert("Powder! Sprinkle it on a dancing pickle!") + text['item_get_flippers'] = CompressedTextMapper.convert("Flippers! Time to swim!") + text['item_get_power_gloves'] = CompressedTextMapper.convert("Gloves! Lift up those rocks!") text['item_get_pendant_courage'] = CompressedTextMapper.convert("We have the Pendant of Courage! How brave!") text['item_get_pendant_power'] = CompressedTextMapper.convert("We have the Pendant of Power! How robust!") text['item_get_pendant_wisdom'] = CompressedTextMapper.convert("We have the Pendant of Wisdom! How astute!") - text['item_get_mushroom'] = CompressedTextMapper.convert("A Mushroom! Don't eat it. Find a witch.") - text['item_get_book'] = CompressedTextMapper.convert("It book! U R now litterit!") - text['item_get_moonpearl'] = CompressedTextMapper.convert("I found a shiny marble! No more hops!") + text['item_get_mushroom'] = CompressedTextMapper.convert("Mushroom! Don't eat it. Find a witch.") + text['item_get_book'] = CompressedTextMapper.convert("Book! Are you well read?") + text['item_get_moonpearl'] = CompressedTextMapper.convert("Moon Pearl! Rabbit Be Gone!") text['item_get_compass'] = CompressedTextMapper.convert("A compass! I can now find the boss.") # 60 text['item_get_map'] = CompressedTextMapper.convert("Yo! You found a MAP! Press X to see it.") - text['item_get_ice_rod'] = CompressedTextMapper.convert("It's the Ice Rod! Freeze Ray time.") - text['item_get_fire_rod'] = CompressedTextMapper.convert("A Rod that shoots fire? Let's burn all the things!") - text['item_get_ether'] = CompressedTextMapper.convert("We can chill out with this!") - text['item_get_bombos'] = CompressedTextMapper.convert("Let's set everything on fire, and melt things!") - text['item_get_quake'] = CompressedTextMapper.convert("Time to make the earth shake, rattle, and roll!") + text['item_get_ice_rod'] = CompressedTextMapper.convert("Ice Rod! Time to chill out!") + text['item_get_fire_rod'] = CompressedTextMapper.convert("Fire Rod! I'm burning for you!") + text['item_get_ether'] = CompressedTextMapper.convert("Ether! Let's cool things down!") + text['item_get_bombos'] = CompressedTextMapper.convert("Bombos! Explosions, fire, burn it all!") + text['item_get_quake'] = CompressedTextMapper.convert("Quake! Let's shake the ground!") text['item_get_hammer'] = CompressedTextMapper.convert("STOP!\n\nHammer Time!") # 66 - text['item_get_ocarina'] = CompressedTextMapper.convert("Finally! We can play the Song of Time!") - text['item_get_cane_of_somaria'] = CompressedTextMapper.convert("Make blocks!\nThrow blocks!\nsplode Blocks!") - text['item_get_hookshot'] = CompressedTextMapper.convert("BOING!!!\nBOING!!!\nSay no more…") + text['item_get_ocarina'] = CompressedTextMapper.convert("Ocarina! A Flute by another name") + text['item_get_cane_of_somaria'] = CompressedTextMapper.convert("Somaria! Make blocks, throw blocks") + text['item_get_hookshot'] = CompressedTextMapper.convert("Hookshot! Grab all the things!") text['item_get_bombs'] = CompressedTextMapper.convert("BOMBS! Use A to pick 'em up, throw 'em, get hurt!") - text['item_get_bottle'] = CompressedTextMapper.convert("It's a terrarium. I hope we find a lizard!") + text['item_get_bottle'] = CompressedTextMapper.convert("Bottle! Store all manner of things") text['item_get_big_key'] = CompressedTextMapper.convert("Yo! You got a Big Key!") - text['item_get_titans_mitts'] = CompressedTextMapper.convert("So, like, you can now lift anything.\nANYTHING!") - text['item_get_magic_mirror'] = CompressedTextMapper.convert("We could stare at this all day or, you know, beat Ganon…") + text['item_get_titans_mitts'] = CompressedTextMapper.convert("Mitts! Lift ALL the rocks!") + text['item_get_magic_mirror'] = CompressedTextMapper.convert("Mirror! Take some time to reflect on this moment!") text['item_get_fake_mastersword'] = CompressedTextMapper.convert("It's the Master Sword! …or not…\n\n FOOL!") # 70 text['post_item_get_mastersword'] = CompressedTextMapper.convert("{NOBORDER}\n{SPEED6}\n@, you got the sword!\n{CHANGEMUSIC}\nNow let's go beat up Agahnim!") - text['item_get_red_potion'] = CompressedTextMapper.convert("Red goo to go! Nice!") - text['item_get_green_potion'] = CompressedTextMapper.convert("Green goo to go! Nice!") - text['item_get_blue_potion'] = CompressedTextMapper.convert("Blue goo to go! Nice!") - text['item_get_bug_net'] = CompressedTextMapper.convert("Surprise Net! Let's catch stuff!") - text['item_get_blue_mail'] = CompressedTextMapper.convert("Blue threads? Less damage activated!") - text['item_get_red_mail'] = CompressedTextMapper.convert("You feel the power of the eggplant on your head.") - text['item_get_temperedsword'] = CompressedTextMapper.convert("Nice… I now have a craving for Cheetos.") - text['item_get_mirror_shield'] = CompressedTextMapper.convert("Pit would be proud!") - text['item_get_cane_of_byrna'] = CompressedTextMapper.convert("It's the Blue Cane. You can now protect yourself with lag!") + text['item_get_red_potion'] = CompressedTextMapper.convert("Red Potion! Heal yourself") + text['item_get_green_potion'] = CompressedTextMapper.convert("Green Potion! Magic refill!") + text['item_get_blue_potion'] = CompressedTextMapper.convert("Blue Potion! Heal and restore!") + text['item_get_bug_net'] = CompressedTextMapper.convert("Bug Net! Let's catch stuff!") + text['item_get_blue_mail'] = CompressedTextMapper.convert("Blue Mail! Less damage activated!") + text['item_get_red_mail'] = CompressedTextMapper.convert("Red Mail! Even less damage!") + text['item_get_temperedsword'] = CompressedTextMapper.convert("Tempered Sword! Even more slashy!") + text['item_get_mirror_shield'] = CompressedTextMapper.convert("Mirror Shield! Time to reflect") + text['item_get_cane_of_byrna'] = CompressedTextMapper.convert("Byrna! Swirly protection!") text['missing_big_key'] = CompressedTextMapper.convert("Something is missing…\nThe Big Key?") text['missing_magic'] = CompressedTextMapper.convert("Something is missing…\nMagic meter?") - text['item_get_pegasus_boots'] = CompressedTextMapper.convert("Finally, it's bonking time!\nHold A to dash") + text['item_get_pegasus_boots'] = CompressedTextMapper.convert("Pegasus Boots! Finally, it's bonking time!\nHold A to dash") text['talking_tree_info_start'] = CompressedTextMapper.convert("Whoa! I can talk again!") text['talking_tree_info_1'] = CompressedTextMapper.convert("Yank on the pitchfork in the center of town, ya heard it here.") text['talking_tree_info_2'] = CompressedTextMapper.convert("Ganon is such a dingus, no one likes him, ya heard it here.") From 05f4ef8c3cec61bafccc4b95e7139e79494f4228 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 21 Jun 2024 09:50:58 -0600 Subject: [PATCH 23/28] fix: various enemy bans --- source/enemizer/enemy_deny.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index c8383fb3..04be92fa 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -34,8 +34,9 @@ UwGeneralDeny: - [ 0x001e, 4, [ "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "BigSpike", "Bumper" ] ] #"Ice Palace - Blob Ambush - Red Bari 4" - [ 0x001e, 5, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Blob Ambush - Zol 1" - [ 0x001e, 6, [ "SparkCW", "SparkCCW", "RollerVerticalDown", "RollerVerticalUp", "RollerHorizontalRight", "RollerHorizontalLeft", "Beamos", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Ice Palace - Blob Ambush - Zol 2" - - [ 0x001f, 0, [ "RollerHorizontalRight" ] ] #"Ice Palace - Big Key View - Pengator 1" + - [0x001f, 0, ["RollerHorizontalRight", "RollerHorizontalLeft"]] #"Ice Palace - Big Key View - Pengator 1" - [ 0x001f, 3, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots + - [0x001f, 4, ["RollerVerticalDown", "RollerVerticalUp"]] # too large, hits the door - [ 0x0021, 3, [ "RollerVerticalDown", "RollerVerticalUp" ] ] #"Sewers - Dark U - Rat 2" - [ 0x0021, 4, [ "RollerVerticalDown", "RollerVerticalUp" ] ] #"Sewers - Dark U - Rat 3" - [ 0x0024, 6, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots @@ -121,7 +122,7 @@ UwGeneralDeny: - [ 0x0049, 5, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Skull Woods - Bari Pits - Gibdo 2" - [ 0x0049, 7, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Skull Woods - Bari Pits - Gibdo 4" - [ 0x0049, 8, [ "Beamos", "AntiFairyCircle", "Bumper" ] ] #"Skull Woods - Bari Pits - Gibdo 5" - - [ 0x004b, 0, [ "Beamos", "AntiFairyCircle", "Bumper" ] ] #"Palace of Darkness - Mimics 1 - Red Eyegore" + - [0x004b, 0, ["Beamos", "AntiFairyCircle", "Bumper", "BigSpike"]] #"Palace of Darkness - Mimics 1 - Red Eyegore" - [ 0x004b, 1, [ "RollerHorizontalRight" ] ] #"Palace of Darkness - Warp Hint - Antifairy 1" - [ 0x004b, 5, [ "RollerHorizontalLeft", "RollerHorizontalRight", "AntiFairyCircle", "BigSpike", "Bumper" ] ] #"Palace of Darkness - Jelly Hall - Blue Bari 1" - [ 0x004b, 6, [ "AntiFairyCircle", "BigSpike" ] ] #"Palace of Darkness - Jelly Hall - Blue Bari 2" @@ -132,6 +133,7 @@ UwGeneralDeny: - [ 0x0050, 0, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Hyrule Castle - North West Passage - Green Guard" - [ 0x0050, 1, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Hyrule Castle - North West Passage - Green Knife Guard 1" - [ 0x0050, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Hyrule Castle - North West Passage - Green Knife Guard 2" + - [0x0051, 2, ["Zoro"]] # Zoro clips off and doesn't return - [ 0x0052, 0, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper" ] ] #"Hyrule Castle - North East Passage - Green Guard" - [ 0x0052, 1, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper" ] ] #"Hyrule Castle - North East Passage - Green Knife Guard 1" - [ 0x0052, 2, [ "RollerVerticalUp", "RollerVerticalDown", "AntiFairyCircle", "Bumper" ] ] #"Hyrule Castle - North East Passage - Green Knife Guard 2" @@ -154,7 +156,7 @@ UwGeneralDeny: - [ 0x0057, 13, [ "RollerVerticalDown", "Beamos", "AntiFairyCircle", "Bumper", "Statue", "BigSpike"]] #"Skull Woods - Big Key Room - Blue Bari 1" - [ 0x0057, 14, [ "RollerVerticalDown", "Beamos", "AntiFairyCircle", "Bumper", "Statue", "BigSpike"]] #"Skull Woods - Big Key Room - Blue Bari 2" - [ 0x0058, 0, ["Statue"]] - - [ 0x0058, 1, ["Statue"]] + - [0x0058, 1, ["Statue", "RollerHorizontalRight", "RollerHorizontalLeft"]] - [ 0x0058, 2, ["Statue"]] - [ 0x0058, 3, ["Statue"]] - [ 0x0058, 4, ["Statue"]] @@ -407,7 +409,7 @@ UwGeneralDeny: - [ 0x00f1, 3, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 4" - [ 0x00f1, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 5" - [ 0x00f1, 5, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight" ] ] #"Old Man Maze - Keese 6" - - [ 0x00fd, 0, [ "Bumper" ] ] + - [0x00fd, 0, ["Bumper", "AntiFairyCircle"]] - [ 0x0107, 1, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] - [ 0x0107, 2, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] - [0x010b, 6, ["RollerHorizontalRight"]] @@ -454,6 +456,8 @@ OwGeneralDeny: - [0x5e, 18, ["Gibo"]] # kiki eating Gibo - [0x5e, 19, ["Gibo"]] # kiki eating Gibo - [0x5e, 20, ["Gibo"]] # kiki eating Gibo + - [0x62, 1, ["RollerVerticalUp", "RollerVerticalDown"]] # hard to avoid roller around hammer pegs + - [0x62, 3, ["RollerVerticalUp", "RollerVerticalDown"]] # hard to avoid roller around hammer pegs - [0x77, 1, ["Bumper"]] # soft-lock potential near ladder - [0x7f, 1, ["Bumper"]] # soft-lock potential near ladder UwEnemyDrop: From 44ef13c0f6050bc4b2e8e56235a5b674114abaf0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 21 Jun 2024 10:35:52 -0600 Subject: [PATCH 24/28] change: update baserom --- RELEASENOTES.md | 1 + Rom.py | 2 +- data/base2current.bps | Bin 117480 -> 117948 bytes 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c8a6ce5f..b33ee183 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,4 +9,5 @@ File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcod * Hera Basement Cage: Fix for small key counting multiple times (again) * Generation: Fixed several generation problems with ER and intensity 3 * Customizer: Generation bug when attempting to place small keys + * Hints: Updated pedestal/tablet text to be more clear * Enemizer: Various enemy bans \ No newline at end of file diff --git a/Rom.py b/Rom.py index 249ca8c3..be1353d9 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '5f039a5d88474aefc66c116beb67fa68' +RANDOMIZERBASEHASH = 'b9c880298780c650ec1deb14ef8c533a' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 05c1fd24b5c0eba3dad03a9aa19f3d6582f55981..4050df7b1ecfcc913a464cb5cb7f1f3da68dd91d 100644 GIT binary patch delta 21691 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ÐY;I`^UeRdmb;&YFs{)DoL;9)$!U}gu^NM z`IrhXB!70b;$XoEcEzOk)H8PW#P`(+IZZu1dBV5p#*wg-*g94(+-AV@V_Ii-`?!B{EEyfzi+%|;dL zswwd`@TEK^lZ`$`Gte5-aD;)j(yi@~O_d_`mI3G-JOWyn)!)CK$AhOB2%IcIG#X># zzbY|+4E}Xrdp;)JSd80Cqf?F-6f!?hagDO;W8f*#O-hlU)i8BxDi-;{VA#2cNs~Q~ zdkv+~uCZE_DSaIWH1z(gwVx9j$5T{14u`WB8kwpTZYst3)B&}u;Cs#Gf-+q>mCzU^ zvu-~!stjsWK#dBhQTG38u%NFgMZW8m!N9(zV%C#_$2J{c^MH>{rGQFBO=W<}L`@ZJ zUQ>n0E4^cmfG2e0@g|fe4|@}Vq~dk>bZbQDO(y5lZ&uvBa3K%JKSOEjazDRBt&SWc^N<|D(G#voQJS%vm zfQMSsWdP5CfjNW+CQ{=6B*q`XJQ6Yy2()ao8>_}4(Uf8nEJotF$b$mgAIh&?gF2u< z`KB{K!Q4DAj!VzSmroAofAtLib+V8@eJqxr8fKS5d|+1*xb;+`z)$)Y{&*^XT!0gZ z&On~oQN5h_@HHC1laSw^1SeoFbzXRoYyWtQ>lTugxoF-^ZvIW~(M@jCE$-i2+<&+5 zxzoP07 z5KTkkoxipRx&1hUN{jmLc!!hO9<)c3r-kpEo?fvop#AzJyyZ*;=wan&hPnwRQ`@I~ zoG2rYBSb#J*Ux)Tol5){B@hYDoUnn3lEZX(qY$I z@q!^!5uSaEbRISH;di#4C%n)E3Ep+4?V+iEZr(Xi<9o~Z)>`-txnEpje_eVj<+ic#j&1g9o9vowc&PFAv)g;;pr`K0 zBwu|e3?(3HZ6vzJn4{5XbnOsD{qw&z)t%EWcY5x)-&NhMzdPja;+DyG?h~%NkZUSZ zq`ynkMeiwUiMY4XhB%B%ABKhR4asFj)PTaEB@kcBYYg9`TU2{0J*mqW^Lw);-U#js z%P7sRDE$0k=oqW<3#M{khS>W~!JM>baz$4`(_KFK>=c*usiq(U*=30TF~do-KC5$? zkf^t>z*8R0^3mNAAG?*_E!6VGx=&c3_U7eb?vW313Aa9)AzeN%_my&XxRQIBXU1cA`PkfZ2^L=? z1)8F#aTn;e4h9=My_FUk1gO;CsYgB7^Ag_4`(KG6!Mz9!+Cr1BQARc6eUJYa`T5tm z?5ZRrHxO*ZZ8}OLfU64@@ zfSq(_XzN;Bq+kXYQG<0d#npAxg}kfjEszJ_!~g_dGYMhwUOEK?;f;z=ahGg zlY4KS7oW+wyg64rM*$lfMn;`FJioO~v(%bos>(au*TFc~sg=ysyh{yEp!HdtVys?T zKMjEN?t@e3;Pzn@Wn>IB4CFBTA(vgbE4S*25lBB~f1}%OJoo2Vn;k`(4OsSbWc-W= zT8)|-!pN{>S1uKfWmGsLdl(K|5M1QI2io**@$9&lV;kBYJ&4|pBdL-It7ad;CgWdz zjwL4ILERzJgmk)YTxykOcxqMg_Pw_ha3(~(YREGx_bqwdJGhC;-qd*Zowz_hlOneCuNy!}3yFsYEc2Mq5yjrA2iHZ%g1?!1Z9@IU>!(QYr)Uvf2G`jlan&Dr^8C)NjtK8u#RZs;s1Zz3;e8iJXi%b5~`v~k(q z>y)%nb}rQjPdD{j>>FOS^SEY-mR%q9$8?#r=ZREl1D+GRYf`Sc`4!zP((>xd#D8{K z{K2&Fv*v;|&V&4(`utC+ZbK>(KpFTJxaL>HYn zik^UyJNpXy&=*0ZgZ)My5owl1|G~;&elR(goS-DB>4>Ny!}X|01NWvk?toZjyNs%3 zWK=&RV+Q1^hbi#b=h1fG=2OFTWAMi3lfCbroi_6>vAcSy9blWPakvC+gwnG{&tLGwS(0L(_u3|r{{EY|B#KH%VC$4>9)!;2o zMp!f^!wOS4m$SAlK=7mS(jBq4V_(Kg!fSWmWtu&VT@ZoA z#uxr+bMJ_EJ`}4Tl2JcuyJOgetfFyGAM@}ydTuqRw}9~j9(nKfpl|bPt*LaVEpaH_ z8&$!4rEdze`c(N$Bt+b{;K+t1vmHDETmf7#uS=a^#*x1S1cM9K>U(8IbYym6*TYqO z6n$Q7;Mp*Fa%LpEGW9C{A|7x2MLeMUQC9==tK34ctG>vS3;S>^#)EOqFW$rieBzfO zQ@bCjzrvI&>zSU1SI&JeN6u`<-1m6tsz08lt@=Z5{02`v^24Yja)|5gI3XXCfhQcP zA&qyyf#hzyi%YBEoY}yhkie*A*;m0w`qks+p!4F|?xeVv?mT&SZ4bLTr>du0EdC0A z-ZK#V4m*105VP^Qo+LQ^=Jax~Jup(zr%u7cUWy4mp8GQ1uDwD}{(c$LFQ@Ro_QU3v z2@XrT!Cr+>8TReb%05~OdR*Ld6BsOY4bdG=Tg(p z;@oFq!7~_ZG>&^U+QV)pqGDKYqpAy>J8Rg5OL69_*@O)?y$Z3vcY?!X(Omrg)%+o( z+_}!#?Ek{~g%x{$@BF>9anifYTu8rgHYVVeziyT8MDo6LcK+x*iexiRFnjxcb86Vv z@~rJy=d)YS-aPAnW?X1pi~;mXj3I1rLLL>OnQ$5p90>$0t+oIRC1{<2she<3Ds0YO7F@xSz#Ls$wH>TyCis;~(I(U}(D8XvJ zxUn%En9rytpeU2Arm881XNuujB9NcSmB+5YKmHcx^Vf63q^R3~48v;^5j9Y=Q;@G{<6N1K>`Z>X9J#CGf5tpr z4TKNCe6Q+oReRYWybe)w*ib<}wq2&Ev-wOh1nu}tpS*R`;f!5TRqCcS!)(_1Qu&DhiPo3)1-(7z-h~ z`k1jop~W8jrHQQMqaqRPj17~)L;6x~@%c$Sy^fM-yfg66uS3@leoRT&-Cehu{uMyz z9S%e$SG{anA&7v;4p3~wn7pbOkU2qe#Y;S^;p!GshDAbteVSGX%wJjqQ9P>ovQ|<3 zQu``L!M)T5p|2D3E`hEmu?TryU=hUF8N_rn!P?-jEfFEhmpxYux2kT(E=7aY^^qQ7 zXLw1Ev@>YwQFf(bTXaio7>e61QG^ixW(f)zAKt}&71l*(_5ah)22CqdWc9bJ6Q`B2 zP@uZd-`=0wxG@Zm{$sV@l4q9z zY|K85i>{io%d*O{oCLf?W zs{vpebC0Q8Mg6r;M!m=6tt^GC zW6GZZT9j@ob=EM|Y=9Z69DwR9J^p?)YF2#*i62#W#{qh#dIj)}ieIeG@+|fPNEVL- zm{`07;8(>viy1}hugoTS?F5txBhM)=E^gh3h407mS9#-E?I|JmZY zL07)>^UYuESdrEmsK*_Dj_2=;(`bI*vyVS^_1Z7jY+bea-WGN?p777td_fI1{*xtM?_)NsU$xQ*!of@U!@C z>MWhOiw&ggd9SK$gk81A&9;VwiAZ2bnJ8~L70t?9PsOnElXb1~)2D{95$-&TV?*uf z*`uzQ|NWE$ifDw0*U)~pSQl~{a-!IZ>EG7QMLdfOgv^?Yb(c;z^W~>&Pm>RdaqoYD zg5*K3u=~dWj?<&ea>D?2b`*~KSeP(9HNU*^Ky9b*dLBzI%2(L(UaQLW%~0%@^y(`+*g_=_!XTO0&!lfb(4@o%k<>3QhkNrl+%XPt~2etlIFfD z2O%4=58>_mQRz2d=?nf?fN*jD82)yIf9R(K`##r|%qIf*-+W!Njp*br8^=x{h@c@` zV&s0PI4rFd4~{KTIZLWI=i-9$0HY3ko8H$xXEWiT+u#A-MNQpe@H&=I&Ga_{NvTbE z3A6uaY8c+iz9ER=KH`%c>>Od5@L5Gq^Q$&f&GO{Fw8`ukKJkm2>*`$kS}7db+PXDY z=f*|~h`SEM_ua{Hibjaj*dx{!(%Hv0#K0MNJ9W#&NW+f!kpYMttu&}y zP`=2i*U*z^v9li2wOG>G#m>FfgJE#2fv+8KrLM5bXo4W4{SzBJmz`)!Nc|?IS=eMa z`3pTr!Xv2?n+yXX$zazlD`aom5`pe+(YZAX;~6l({LpMXm9?`Y77QAqZwMOIMN2>* z(uGE4<3YRMDWp3obsW3Zj_@X~vT{3OC9#!#ZbvM32pN>y*n)TZvs3JesY5D{b8L&r z;$U0?Y6m|z{$oNa_2hU~-z@|`3OU2fk;f0~ z$+!b`BoDQdRoQU*46r;AR!~!#STR9tQ}M6Q{+TB2aMpt;r5Lb z2TlT&Q0^~D(#>Ct3Od@)$&EuAC$-9r^VD-&@v?h(=;$H^-(ttE`YaoN^;tG>G>rX{ zJ?B6S@bUZhU9ljOqU=g}-_R-vbLnlS+E&2+>Oe%hMlC?{ZSUA`=luC!+AucUkr+Y* zvq_GGKe35j>qx|mx69XqRcv{e+2ll+os1q}gWHgw%Q$jb7RQQ1^Kirf1;oTw&r@i2 z3$jbd_-xLYtG54&zBP&cpCd6k@rNc3BAKNV7gV)PR3HiYPb-%;<2pyeg{Q=#-)s%u zat{fdz40`^U*jS9ROSw2hXAVsp70x`#PRdknNCF1hTVg5YwW-XuTJ1g3fOVMc_l`O zHCcr08bhu1O9n8%Ak7Y%{=gPF)J9uu$W^UYnGdyUzRb;cnGh`#*f6)>WkROM1s|tZ zcxFcSIk(D}U_3LR$~C6hUdlT!#!2iWCt@}c$3{34l4*{E@0yYi7bIUUu*Pidw9hb* z@$u}w!FT0{3z!`0R|qD&gX>?7PHuBR%}_4PNx9aW$uFMqpDkrxAwJ9AaV947=YGTb zx)6!{q#f)RF2q8@pFQG2cuThza}R{#!v!c@o2|J?1=Q718#QH=qxwL}6k_0TJ@f(YI)1r8t1srB35`<)Tg4yP zzrmw+SAn@-H97b@^?U^0pSyH6=6ck5><4wNloBwbk?LQ#!-KU$>?8GK%7!_sdIzU; zJ=uI#U$6<4AA^L4aXK!l(w;5m(mjkb*xji7VllVI!#I;ID*v&V%kVHJ0d=#O+vs7O z1=Ir=mj}euiprls$32X5fcmwV>+vwoW!+J^4fM#vIFFr8m%G4meNSTwOP70NZjGmL z0Z{&!%kVTVWZfxh5$qxgp^0Gn=wX=Kh*PN=S8Xs*p2kHu6`kVV2#sklQ~~SmM)DK{*Gkws7l}wm`s!&@Ib(atUS}blp9kuHL7En+wH;R zfr@d+EGx;!}u8+kzbw+6+8otORfCb zu8BdaG|c&Xv5qoou{yp8*VcfS#96zBUFA;1_{<%itw|o9ts3g(M-OB3@%YiS#%VNL z=T3~YbxnG#nb7*!owz{|TDHZ57)4aFJs!jqhgnI_sK3=;gt4)ngm#GiuEK{a9lB%S z?DE+Y`aON&*2yr>usMJ(@?ukWvu`|!@x(QDxEJxvJ(8bAjd&_+-%ezyqlhdv(3==O zE|i}o*P8M9qHz=@xg06TVk7;X(UF8A{xmJmuE-W(bYmi9mB3m6?jhZxuzR<_SL4Cv zc@wex?LO=oZz9k!(D|W)8zy8wW1o8y;)!PqCAFPq8-oo5iFJe?() zX>9)h;xN(89`+#y**~r*NMiSev;XrU;=MC>6+r{C5M;MNYT)zaHspT>)_)+ezHG{1Lq(G#S6%ki{4&Gg+C68b=*afP=MqP|#@&;mwl1#4^Hz-Rn!FOG~TI$7mC5 zN)vaSGN`p@Z4B{l47s^%N*&38KnggX9kIIv8GQU+jwy4K$s;T>I zQ;qHruBFblJhMYr)H{~aa&xE;8trlbS1KjV|XRN}~QpJIQ950pf^U`C~S;1r57;FUSlG~FH_O2J11yA}va zbsJXFRkp_5H{c}|t4FV-o2GYJ&x_RNM^#c0gIbm z+uhl=K|~KBU=R8cBNp*>=)qQ9r>{zCJf-YINL^tLKo#9Mp=RO|qJ07pI zQ?q0%eacsz{waZM`}?(vbeiN1^mIh1(`y~zG=N!Pec}9c@t17mU?QA2#V#I9BuLd? ze{%(*+PpxxZq&A{T_>Xt@^}N;&GRqFc|+7muK5+KqrY#Fop6pjNS#)X9NVal&3RaN z)Udt5_YjXKhKtAZYvAz`fFfCkXil?b)%mJJCl0-UjNe;A#-I|XCLYhTNxj$ZkUA~) z--^WnREzAFusw3q(!+yR9=0~H4&atgR1@p!lT|m&EuW3{Cl(2oct2<5{=^wV)H)@A zSW6I1Y@LXRaPTWT0wdydt#?I4p$(DGt_>pch)L}4K}0o?z|z4)hX2I1^mPHxFbYmO z8$9{UcjGhD-H|*0Xe7IHHR};VEL<`!6fH*%Tx_s*L@=^zn)X(lezY+4a#7ml^Wvt} zZ^fBMYi$~eQZCopHWV$qTx-`*wD@wZeM8Y_mmvV+E2I1vwlM_`0PeyvI{k9dY9{Zc zK*XL2A^eDJ_I3y{(ar|J7a0-kpipAS!t|pZ%3$cUR9v{`a?w|pnIAWyDB}dw>>dv% z(gl{lgM-0Y!aLF(V~v0gZuW@y*f6`kD^KExG1{a^9hFMCjnR%hddkkg3x)?k3+KfW zaVC2yoCqamvcHBC6Qk{V${lOO#g53m@z*L22$e}*6<}(W?mKo|B5rA2)49fE-G$7~*f*bG26WGK+{csz`^Q$r_d4zm3q%S(^O>5(ye;MczWiQ%Ku5SDNxZk5+|`bD_Q zJ2j!L-wY?#@jaLB;F@^+Rq&-Xj}N$3?Zo~XLzFqj3ekTqi=<>IOUDxa_LGEY33}Pi zHpdcr>5}$O=lo0BHDB*7t9DsisM-PN^V{1!U1h}+zP?}Q5p#k1pc9i_MV_0GdR%0V zVt}!p4H}qB+k!!_@wU&sXV)#RHoHQMvDU-vZz$ymGU|Iy=~4u{-CZQ%N;^M4zle|h^3HA$$Eh9l*)if#0At~c z7vJ?_UIWJyC$fV_5~66R>>1JDFLZ`eG=5G_R+#SOln&K>*LKUht#|vb?UDCLYW=!} zVlA67l9=gM*O8tPm+nyDT%8u46U(-ZB*rg_ZQ3Wgx>sb0xVlHA3^09EdeM7D-9iIT zYhSfhbVb;AUWiqtQ`Bi=i=JEF7_DbBpTJ@71anu0!5xKJmo5i$5F5P;4PbAK zBBBLtTScthXks*Rf}Jp$5R?92qM{WSYJATd{y8tKMX(JZbcotWh1lnfg;pEN2-_|>ry>mrT6@kO>Q>r{lYEVF`Rge&5eMua3U576 z=x{|SfN$+M`;2&WBk5IA(|(coT8AjzFZvUJS<-VU`$px_L51lW?jgR#s)P>yAl#fRH+y zdZU)L^>)V`jwC>mZd_{mR%F=@_8N7Yl^ws<^#65mBpcu*dJ72v`gjorLOh5Ihx7be~-l3QdvJ zH>8c7Na8kZACtIp0ce;30xV8Cv_bAMq8Mc0?(hh(V2=dp$a?h0DiDY@V-&BHH^jk< zb2Do&6nZ37)1>l=9i7xXd1+LizWP8pc$K#*3Vc3HrDKi_{{n{7Bl{oPX#me_o{=e;TFLy*IK!&PN6%p zYx*8ZIm)m+X5R@9tU3tKNlv?4>t6%~_4uhqXZXL4H9KHZ4dr0AKN!NJH%C7lYVr1_ z`n+vU6Z4wqO+*aw?J+sigO728jXB#)y)*=ACNzscDta_};jZjnjL`Z={za7`&}%ez?ES2q-C{(9Tm$cf z*4?>aV}8baTThMkQ5vDpB0`VcATIuGoS;iG-vqR?fX|ERsp-#52ZFIM!vn+dmbAx;3%VtBn;U^VkjEDpE&u==vrdP-Tdd{ zOOqqUZJ4$5?Ae_=)PF|IDT$L9CDWropU1<3B}IioICS~ zkmz6MTi!I9H=z3o;77(DK+h8(n0)*%5E(70`9`Ey?bcLp_7fVWt@7HLhsrNxd0M$V;d}kRP^gM z2;(_csJll`Q?~h-r*7Jh_Mt^NAR;%Gp!6K@Wu|&*jg{C+%>h3z&8c&87 zyxR2aZq5XcK&k7{C;9MqGA9Xz7r=a<6=8R83V#t0*n1S0#^77?+@}I#87eCPzGpf< zM~h(y-TSN2Y3G32g>W@U2z0gp7JF7ka4B^bk=ME?v7zP29r-RpssueMfJJ291ajGq z1Kl{ivK^+`f;e)Pey}r_Ub+Lk8EnYbDk=BPid>Yj3m%Xe1!&=JSWYf>qYHO~7@U{G z+bgobi}NVQEvRV^?8`2Grg0)d?a{X#tW#&iEU{BXmgJby33C9kX7exiDmjfdoV>r( zyxizPZ5(r9f3Mh(81Y6wX^{+ySf-f8 zZw&SFg>MxmG^ZHmai%WKw8Xw}q*BIFbTOWE3RG4Mi9uzxS~x3VshXPRD`Hqm?u|m0 zHE6Pvc;Y|gZN7@`7sGsqzlH_xpqXNj!dED+1X5y`s1ZkTUbd@xMod`6lf(EtB}akc zqpnjLTw&|<4L^<9QrI-IlK%d9v9PiXQQ1bVk~D`58AN$TUCv5$zXZbP9uDughkL;y ziNGSkC2Z(TVMoS?>aep>pbWxSh40~0O`f&-S$@E&~tOB-L4pGb?mQLbny2#|PKh= zDJ-g99;a4YqbCtRO?}>@o0u?JC8h?$HmQ0E)lE>Gr2cN=@qD@$Ba*WEdn1BbnC&&o zRm52ajMJZ*5Ziv95e;kWP7lwMs6wQc>L0AlBBB{X7>~_rjXGAZ&Ovv}z%MIur*Zf|v0m;) zE7~lAKH)XWgRcD$m+oK1|DtrdL9yDiGcO^&a7wv{h2n)%0~r-6{ZB1FwG7B~%j%I( zc3tg6q#JfzL&7KpE@m18XiYhYRxi%*!jWt2a>tG&;bH?&!0cC{0m<3TajaHw4Q!Bh>hR6(cv-=a=- zXY3(ZcKj%8HpH%_4_1SU!JIe$hHl+Xno$Fbo!I_+detJkuOWk8(*Ow$jzvY+y4ZA0 z6I>#FkFWj?Wyf3O9+y11rfCNEBW1>tf&CsI(VOsbjf^>45_;9n)m<$+)N;30cfT85 ziHOpJn&n^GE9VMWu0 z7WOUI_?VVJ&HWb|GOkahVZ`-a(Ky6mv(7QDkLO0saj3aJgp&}A{LN5E z9;rmf&G0+JrD_}<9)&bAGl!lS#d|9|E?~69`Jh^tiYjhFI=5;=dq*1REh(TMDNRLR z+=6_6#{SFWjdW(&<*sGX*E>$>(zd8uxX`N064UWx&<+ttgI`}cr6*#DctT~!Bn-OZ zdEK)WBq1W)+t;nXtz;0bYw<1GY$Wqf(VTJUhJc069TyMrE?@h@{)_#zJ>+d$$+}xr z^7X@8-&LZ|K7|@_%Z}sfR({xpMzez>L$z9k4PVe;p K_N2$$(e^(n36J3b From 27e792d6c4238a78158e1b04aa309997826044ee Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 21 Jun 2024 14:12:31 -0600 Subject: [PATCH 25/28] fix: baserom issue --- Rom.py | 2 +- data/base2current.bps | Bin 117948 -> 117944 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index be1353d9..12299b85 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'b9c880298780c650ec1deb14ef8c533a' +RANDOMIZERBASEHASH = '40ce790628b55a05ec749f476198cd8d' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index 4050df7b1ecfcc913a464cb5cb7f1f3da68dd91d..e33c04dc7c9c7c413a4e5c5f6a82d263d9a27862 100644 GIT binary patch delta 4199 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 VJ)E<3f>7JA}Op|N<_{{d#T1q1*9 delta 4200 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 Date: Mon, 1 Jul 2024 16:00:35 -0600 Subject: [PATCH 26/28] fix: credits crash --- CHANGELOG.md | 13 ++++++++++++- Main.py | 2 +- RELEASENOTES.md | 13 ++----------- Rom.py | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a306a6e6..08bc1eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ +# Feature Notes + +1.4.3: File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!) + + # Patch Notes Changelog archive - +* 1.4.3 + * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues + * Hera Basement Cage: Fix for small key counting multiple times (again) + * Generation: Fixed several generation problems with ER and intensity 3 + * Customizer: Generation bug when attempting to place small keys + * Hints: Updated pedestal/tablet text to be more clear + * Enemizer: Various enemy bans * 1.4.2 * New ER Options: * [Skull Woods shuffle options](#skull-woods-shuffle) diff --git a/Main.py b/Main.py index a533cb7b..b9ab774b 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.3' +version_number = '1.4.4' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b33ee183..752187f5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,13 +1,4 @@ -# New Features - -File Select/End Game screen: Mirror Scroll and Pseudoboots added (Thanks Hiimcody!) - # Patch Notes -* 1.4.3 - * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues - * Hera Basement Cage: Fix for small key counting multiple times (again) - * Generation: Fixed several generation problems with ER and intensity 3 - * Customizer: Generation bug when attempting to place small keys - * Hints: Updated pedestal/tablet text to be more clear - * Enemizer: Various enemy bans \ No newline at end of file +1.4.4 +- Fixed a crash bear the end of the credits when total collection rate was over 1000 \ No newline at end of file diff --git a/Rom.py b/Rom.py index 12299b85..f3067d49 100644 --- a/Rom.py +++ b/Rom.py @@ -740,7 +740,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(cr_pc+0x1e, 0xEE) # slash rom.write_byte(cr_pc+0x1f, thousands_bot) # modify stat config - stat_address = 0x2397B2 # 0x23B969 - old + stat_address = 0x239864 stat_pc = snes_to_pc(stat_address) rom.write_byte(stat_pc, 0xa9) # change to pos 21 (from b1) rom.write_byte(stat_pc+2, 0xc0) # change to 12 bits (from a0) From 21097333644ddc8f5d7a6d76e3d1edca01ddefa2 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 2 Jul 2024 09:51:50 -0600 Subject: [PATCH 27/28] fix: standardized enemy logic for mire 2 fix: applied new enemy logic for gt mimics 1 & 2 --- CHANGELOG.md | 2 ++ Main.py | 2 +- RELEASENOTES.md | 13 +++++++++++-- Rules.py | 19 ++++++++++--------- source/enemizer/EnemyLogic.py | 3 +-- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08bc1eba..707d2f06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ # Patch Notes Changelog archive +* 1.4.4 + - Fixed a crash near the end of the credits when total collection rate was over 1000 * 1.4.3 * Key Logic Algorithm: Renamed "Default" to "Dangerous" to indicate the potential soft-lock issues * Hera Basement Cage: Fix for small key counting multiple times (again) diff --git a/Main.py b/Main.py index b9ab774b..43bb4290 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.4' +version_number = '1.4.5' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 752187f5..e671bf72 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,13 @@ # Patch Notes -1.4.4 -- Fixed a crash bear the end of the credits when total collection rate was over 1000 \ No newline at end of file +1.4.5 +- Logic: Added appropriate enemy logic to GT Mimics 1 and 2 rooms +- Logic: Added appropriate enemy logic to Mire 2 room. Note this does change the default logical strats, due to how enemy kill logic works. + - Ice Rod + 1 Bomb is now out of logic + - Fire Rod + 1 Bomb is now out of logic + - Fire Rod + 1 magic extension is still in logic + - Byrna + 1 magic extension is newly in logic + +(One magic extension is either half magic or a bottle with the ability to purchase a blue or green potion) + +In general, making up for a lack of magic extension with a few bombs is something that could be added to the logic. Using the ice rod to freeze an enemy and then using that enemy to deal blunt damage and then using bombs to clear the frozen enemy is another strategy that could be added to the logic someday. If these are important to you, let me know. \ No newline at end of file diff --git a/Rules.py b/Rules.py index b642f50a..ebe639a5 100644 --- a/Rules.py +++ b/Rules.py @@ -517,11 +517,12 @@ def global_rules(world, player): set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge Hook Path', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Falling Bridge Hook Only Path', player), lambda state: state.has('Hookshot', player)) - set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or - (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs - (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish - state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... - # byrna could work with sufficient magic + # Note: new enemy logic doesn't account for Fire Rod + Bombs or Ice Rod + Bombs yet + # set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or + # (state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs + # (state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish + # state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ... + # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) loc = world.get_location('Misery Mire - Spikes Pot Key', player) if loc.pot: @@ -603,10 +604,6 @@ def global_rules(world, player): set_rule(world.get_entrance('GT Ice Armos NE', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) set_rule(world.get_entrance('GT Ice Armos WS', player), lambda state: world.get_region('GT Ice Armos', player).dungeon.bosses['bottom'].can_defeat(state)) - set_rule(world.get_entrance('GT Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Mimics 1 ES', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Mimics 2 WS', player), lambda state: state.can_shoot_arrows(player)) - set_rule(world.get_entrance('GT Mimics 2 NE', player), lambda state: state.can_shoot_arrows(player)) # consider access to refill room - interior doors would need a change set_rule(world.get_entrance('GT Cannonball Bridge SE', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) @@ -1452,6 +1449,10 @@ std_kill_rooms = { (['GT Petting Zoo SE'], [], 0x7d, [4, 5, 6, 7, 8, 10]), 'GT DMs Room': # Four red stalfos (['GT DMs Room SW'], [], 0x7b, [2, 3, 4, 5, 8, 9, 10]), + 'GT Mimics 1': # two red mimics + (['GT Mimics 1 NW', 'GT Mimics 1 ES'], [], 0x006b, [5, 6, 7, 8, 9]), + 'GT Mimics 2': # two red mimics + (['GT Mimics 2 NE', 'GT Mimics 2 WS'], [], 0x006b, [10, 11, 12, 13]), 'GT Gauntlet 1': # Stalfos/zazaks (['GT Gauntlet 1 WN'], [], 0x5d, [3, 4, 5, 6]), 'GT Gauntlet 2': # Red stalfos diff --git a/source/enemizer/EnemyLogic.py b/source/enemizer/EnemyLogic.py index 67f6eb38..df8a21c5 100644 --- a/source/enemizer/EnemyLogic.py +++ b/source/enemizer/EnemyLogic.py @@ -201,8 +201,7 @@ def find_possible_rules(vln_list, used_resources, world, player): flag = flag if flag < 0 else (hits + used_resources['Magic'] * 7 / 8) optional_clears[vln_sub_list].append((byrna_rule(world, player, flag), resources)) elif damage_type == 'FireRod' and hits + used_resources['Magic'] <= 160: - flag = min(vln[damage_type] for vln in vln_list.values()) - flag = flag if flag < 0 else (hits + used_resources['Magic']) + flag = hits + used_resources['Magic'] optional_clears[vln_sub_list].append((fire_rod_rule(world, player, flag), resources)) elif damage_type == 'IceRod' and hits + used_resources['Magic'] <= 160: flag = min(vln[damage_type] for vln in vln_list.values()) From a37d783ad8d90d738a316ff75bc270ef72b6289e Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 5 Jul 2024 09:46:21 -0600 Subject: [PATCH 28/28] fix: generation issue with dungeonsfull --- README.md | 2 +- RELEASENOTES.md | 5 ++--- source/overworld/EntranceShuffle2.py | 7 ++++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9bd6aba5..20d75b00 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ New pottery option that control which pots (and large blocks) are in the locatio * Reduced Dungeon Pots: Same as Cave+Keys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents. * Clustered Dungeon Pots: Like reduced but pots are grouped by logical sets and roughly 50% of pots are chosen from those groups. This is a dynamic mode like the above. * Excludes Empty Pots: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons. -* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks) +* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes several large blocks) * Lottery: All pots and large blocks are in the pool. By default, switches remain in their vanilla location (unless you turn on the legacy option below) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e671bf72..087fad11 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,5 @@ - Fire Rod + 1 magic extension is still in logic - Byrna + 1 magic extension is newly in logic -(One magic extension is either half magic or a bottle with the ability to purchase a blue or green potion) - -In general, making up for a lack of magic extension with a few bombs is something that could be added to the logic. Using the ice rod to freeze an enemy and then using that enemy to deal blunt damage and then using bombs to clear the frozen enemy is another strategy that could be added to the logic someday. If these are important to you, let me know. \ No newline at end of file + (One magic extension is either half magic or a bottle with the ability to purchase a blue or green potion) In general, making up for a lack of magic extension with a few bombs is something that could be added to the logic. Using the ice rod to freeze an enemy and then using that enemy to deal blunt damage and then using bombs to clear the frozen enemy is another strategy that could be added to the logic someday. If these are important to you, let me know. +- Generation: Fixed an issue with dungeonsfull shuffle \ No newline at end of file diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 1e255b64..56e1e529 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -942,7 +942,8 @@ def do_same_world_shuffle(avail, pool_def): for option in multi_exit: multi_entrances, multi_exits = find_entrances_and_exits(avail, option) # complete_entrance_set.update(multi_entrances) - multi_exits_caves.append(multi_exits) + if multi_exits: + multi_exits_caves.append(multi_exits) for x in multi_entrances: (dw_entrances, lw_entrances)[x in LW_Entrances].append(x) @@ -955,12 +956,12 @@ def do_same_world_shuffle(avail, pool_def): lw_candidates = filter_restricted_caves(multi_exits_caves, 'LightWorld', avail) other_candidates = [x for x in multi_exits_caves if x not in lw_candidates] # remember those not passed in do_mandatory_connections(avail, lw_entrances, lw_candidates, must_exit_lw) - multi_exits_caves = other_candidates + lw_candidates # rebuild list from the lw_candidates and those not passed + multi_exits_caves = (other_candidates + lw_candidates) if other_candidates else lw_candidates # rebuild list from the lw_candidates and those not passed dw_candidates = filter_restricted_caves(multi_exits_caves, 'DarkWorld', avail) other_candidates = [x for x in multi_exits_caves if x not in dw_candidates] # remember those not passed in do_mandatory_connections(avail, dw_entrances, dw_candidates, must_exit_dw) - multi_exits_caves = other_candidates + dw_candidates # rebuild list from the dw_candidates and those not passed + multi_exits_caves = (other_candidates + dw_candidates) if other_candidates else dw_candidates # rebuild list from the dw_candidates and those not passed # connect caves random.shuffle(lw_entrances)