diff --git a/BaseClasses.py b/BaseClasses.py index f41301eb..3abcbdd4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3274,16 +3274,20 @@ class Spoiler(object): custom = self.metadata['custom_goals'][player] if custom['gtentry'] and 'requirements' in custom['gtentry']: outfile.write('GT Entry Requirement:'.ljust(line_width) + 'custom\n') + outfile.write(' %s\n' % custom['gtentry']['goaltext']) else: outfile.write('GT Entry Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_gt_orig[player])) if custom['ganongoal'] and 'requirements' in custom['ganongoal']: outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n') + outfile.write(' %s\n' % custom['ganongoal']['goaltext']) else: outfile.write('Ganon Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_ganon_orig[player])) if custom['pedgoal'] and 'requirements' in custom['pedgoal']: outfile.write('Pedestal Requirement:'.ljust(line_width) + 'custom\n') + outfile.write(' %s\n' % custom['pedgoal']['goaltext']) if custom['murahgoal'] and 'requirements' in custom['murahgoal']: outfile.write('Murahdahla Requirement:'.ljust(line_width) + 'custom\n') + outfile.write(' %s\n' % custom['murahgoal']['goaltext']) outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) outfile.write('\n') outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) @@ -3396,20 +3400,22 @@ class Spoiler(object): player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') goal = self.world.custom_goals[player]['gtentry'] if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: - outfile.write(str('GT Entry Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) + pass + # outfile.write(str('GT Entry Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) elif self.world.crystals_gt_orig[player] == 'random': outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) goal = self.world.custom_goals[player]['ganongoal'] if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: - outfile.write(str('Ganon Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) + pass + # outfile.write(str('Ganon Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) elif self.world.crystals_ganon_orig[player] == 'random': outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - goal = self.world.custom_goals[player]['pedgoal'] - if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: - outfile.write(str('Pedestal Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) - goal = self.world.custom_goals[player]['murahgoal'] - if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: - outfile.write(str('Murahdahla Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) + # goal = self.world.custom_goals[player]['pedgoal'] + # if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: + # outfile.write(str('Pedestal Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) + # goal = self.world.custom_goals[player]['murahgoal'] + # if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: + # outfile.write(str('Murahdahla Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext']) outfile.write('\n\nPrizes:\n\n') for dungeon, prize in self.prizes.items(): outfile.write(str(dungeon + ':').ljust(line_width) + '%s\n' % prize) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61fc68fd..b7435482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.6.1.2 +- Various fixes for Custom Goal Framework +- Added custom gfx for Pedestal and Murahdahla +- Re-fixed purple chest follower dupe +- Updated tournament winners texts + ## 0.6.1.1 - Fixed issue with Bosses goals in Custom Goal Framework - Fixed error when using Custom Goals with no extra values diff --git a/Main.py b/Main.py index ee5a2d63..a8d2265f 100644 --- a/Main.py +++ b/Main.py @@ -645,23 +645,23 @@ def resolve_random_settings(world, args): else: raise Exception(f'Invalid {list(r.keys())[0]} requirement target for {goal_type}') if req['condition'] & 0x7F == req_table['Pendants']: - goal['logic']['pendants'] = req['target'] or 3 + goal['logic']['pendants'] = req['target'] = req.get('target', 3) elif req['condition'] & 0x7F == req_table['Crystals']: - goal['logic']['crystals'] = req['target'] or 7 + goal['logic']['crystals'] = req['target'] = req.get('target', 7) elif req['condition'] & 0x7F == req_table['PendantBosses']: - goal['logic']['pendant_bosses'] = req['target'] or 3 + goal['logic']['pendant_bosses'] = req['target'] = req.get('target', 3) elif req['condition'] & 0x7F == req_table['CrystalBosses']: - goal['logic']['crystal_bosses'] = req['target'] or 7 + goal['logic']['crystal_bosses'] = req['target'] = req.get('target', 7) elif req['condition'] & 0x7F == req_table['PrizeBosses']: - goal['logic']['bosses'] = req['target'] or 10 + goal['logic']['bosses'] = req['target'] = req.get('target', 10) elif req['condition'] & 0x7F == req_table['Aga1']: goal['logic']['aga1'] = True elif req['condition'] & 0x7F == req_table['Aga2']: goal['logic']['aga2'] = True elif req['condition'] & 0x7F == req_table['TriforcePieces']: - goal['logic']['goal_items'] = req['target'] or None + goal['logic']['goal_items'] = req['target'] = req.get('target', None) elif req['condition'] & 0x7F == req_table['CollectionRate']: - goal['logic']['collection'] = req['target'] or None + goal['logic']['collection'] = req['target'] = req.get('target', None) goal['requirements'].append(req) except KeyError: raise KeyError(f'Invalid {goal_type} requirement: {r}') diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 379a3b65..9588d020 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType from OverworldGlitchRules import create_owg_connections from Utils import bidict -version_number = '0.6.1.1' +version_number = '0.6.1.2' # branch indicator is intentionally different across branches version_branch = '' diff --git a/Rom.py b/Rom.py index 43c3006e..c0335ec8 100644 --- a/Rom.py +++ b/Rom.py @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '72c4b2d00057d1faced32871d8081f3a' +RANDOMIZERBASEHASH = '2039c11b935d3b81f78810d9f4be19d6' class JsonRom(object): @@ -1270,8 +1270,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): goal_bytes += [req['target']] else: goal_bytes += int16_as_bytes(req['target']) - elif 'target' in req: - if req['condition'] & 0x7F < 0x08: + elif req['condition'] & 0x80 == 0: + if req['condition'] & 0x7F == 0x06 or req['condition'] & 0x7F == 0x07: + # agahnims have no target value + pass + elif req['condition'] & 0x7F < 0x08: goal_bytes += [req['target']] else: goal_bytes += int16_as_bytes(req['target']) @@ -1338,12 +1341,19 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): if start_address > 0x81D8: raise Exception("Custom Goal data too long to fit in allocated space, try reducing the amount of requirements.") - # gt entry - gtentry = world.custom_goals[player]['gtentry'] - if gtentry and 'cutscene_gfx' in gtentry: - gfx = gtentry['cutscene_gfx'] - write_int16(rom, snes_to_pc(0x3081D8), gfx[0]) - rom.write_byte(snes_to_pc(0x3081E6), gfx[1]) + # goal cutscene gfx + goals = { + #goal: gfx addr, palette addr + 'gtentry': (0x3081D8, 0x3081E6), + 'pedgoal': (0x3081ED, 0x3081F3), + 'murahgoal': (0x3081F6, 0x3081FC), + } + for goal_type, gfx_addr in goals.items(): + goal = world.custom_goals[player][goal_type] + if goal and 'cutscene_gfx' in goal: + gfx = goal['cutscene_gfx'] + write_int16(rom, snes_to_pc(gfx_addr[0]), gfx[0]) + rom.write_byte(snes_to_pc(gfx_addr[1]), gfx[1]) # block HC upstairs doors in rain state in standard mode prevent_rain = world.mode[player] == 'standard' and world.shuffle[player] != 'vanilla' and world.logic[player] != 'nologic' @@ -1751,7 +1761,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): gen, seedstring = rom_header.split('|', 1) gen = f'{gen:<3}' seedstring = f'{int(seedstring):09}' if seedstring.isdigit() else seedstring[:9] - rom.name = bytearray(f'{gen}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] + rom.name = bytearray(f'OR{gen}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] elif len(rom_header) <= 9: seedstring = f'{int(rom_header):09}' if rom_header.isdigit() else rom_header rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] @@ -2581,8 +2591,10 @@ def write_strings(rom, world, player, team): def get_custom_goal_text(type): goal_text = world.custom_goals[player][type]['goaltext'] - if '%d' in goal_text: - return goal_text % world.custom_goals[player][type]['requirements'][0]['target'] + placeholder_count = goal_text.count('%d') + if placeholder_count > 0: + targets = [req['target'] for req in world.custom_goals[player][type]['requirements'] if 'target' in req][:placeholder_count] + return goal_text % tuple(targets) return goal_text if world.custom_goals[player]['gtentry'] and 'goaltext' in world.custom_goals[player]['gtentry']: diff --git a/Rules.py b/Rules.py index 5368b2f5..ee69a481 100644 --- a/Rules.py +++ b/Rules.py @@ -1805,6 +1805,9 @@ def standard_rules(world, player): add_rule(world.get_location('Hyrule Castle Tree', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) + if not world.is_copied_world: + add_rule(world.get_location('Hyrule Castle Courtyard Tree Pull', player), lambda state: state.has('Zelda Delivered', player)) + # don't allow bombs to get past here before zelda is rescued set_rule(world.get_entrance('GT Hookshot South Entry to Ranged Crystal', player), lambda state: (state.can_use_bombs(player) and state.has('Zelda Delivered', player)) or state.has('Blue Boomerang', player) or state.has('Red Boomerang', player)) # or state.has('Cane of Somaria', player)) diff --git a/Text.py b/Text.py index 07fb2e4d..9eb6027f 100644 --- a/Text.py +++ b/Text.py @@ -1782,6 +1782,8 @@ class TextTable(object): text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^") text['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.") text['telepathic_tile_under_ganon'] = CompressedTextMapper.convert("Doors Async League winners\n{HARP}\n" + " ~~~2025~~~\nSchulzer\n\n" + " ~~~2024~~~\nhumbugh\n\n" " ~~~2023~~~\nEriror\n\n" " ~~~2022~~~\nAndy\n\n" " ~~~2021~~~\nprdwong") @@ -1795,7 +1797,8 @@ class TextTable(object): text['telepathic_tile_ice_stalfos_knights_room'] = CompressedTextMapper.convert("{NOBORDER}\nKnock 'em down and then bomb them dead.") text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.") text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n" - " ~~~2023~~~\nnGanonsGoneWild\n\n" + " ~~~2024~~~\nGammachuu\n\n" + " ~~~2023~~~\nGanonsGoneWild\n\n" " ~~~2022~~~\nObscure\n\n" " ~~~2021~~~\nDaaanty\n\n" " ~~~2019~~~\nJet082\n\n" @@ -2029,6 +2032,7 @@ class TextTable(object): text['ganon_phase_3_alt'] = CompressedTextMapper.convert("Got wax in your ears? I cannot die!") # 190 text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("Glitched\ntournament\nwinners\n{HARP}\n" + "~~~HMG 2025~~~\nSkele\n" "~~~No Logic 2024~~~\ntam\n\n" "~~~HMG 2023~~~\ntam\n\n" "~~~No Logic 2022~~~\nChexhuman\n\n" diff --git a/data/base2current.bps b/data/base2current.bps index 224e1fb6..d052e425 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 80b23e0d..af34f007 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -577,9 +577,13 @@ class CustomSettings(object): def load_yaml(path): - if os.path.exists(Path(path)): - with open(path, "r", encoding="utf-8") as f: - return yaml.load(f, Loader=yaml.SafeLoader) - elif urllib.parse.urlparse(path).scheme in ['http', 'https']: - return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) + try: + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: + return yaml.load(f, Loader=yaml.SafeLoader) + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) + except yaml.YAMLError as e: + error_msg = f"Error parsing YAML file '{path}':\n{str(e)}" + raise ValueError(error_msg) from e diff --git a/source/gui/bottom.py b/source/gui/bottom.py index 7faf3402..e25c2b2f 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -77,32 +77,37 @@ def bottom_frame(self, parent, args=None): argsDump['sprite'] = argsDump['sprite'].name save_settings(parent, argsDump, "last.json") - guiargs = create_guiargs(parent) - # get default values for missing parameters - cliargs = ['--multi', str(guiargs.multi)] - if hasattr(guiargs, 'customizer'): - cliargs.extend(['--customizer', str(guiargs.customizer)]) - for k,v in vars(parse_cli(cliargs)).items(): - if k not in vars(guiargs): - setattr(guiargs, k, v) - elif type(v) is dict: # use same settings for every player - players = guiargs.multi if len(v) == 0 else len(v) - setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, players + 1)}) - argsDump = vars(guiargs) - - needEnemizer = False - falsey = ["none", "default", False, 0] - for enemizerOption in ["shuffleenemies", "enemy_damage", "shufflebosses", "enemy_health"]: - if enemizerOption in argsDump: - if isinstance(argsDump[enemizerOption], dict): - for playerID,playerSetting in argsDump[enemizerOption].items(): - if not playerSetting in falsey: - needEnemizer = True - elif not argsDump[enemizerOption] in falsey: - needEnemizer = True - seeds = [] - try: + guiargs = create_guiargs(parent) + # get default values for missing parameters + cliargs = ['--multi', str(guiargs.multi)] + if hasattr(guiargs, 'customizer'): + cliargs.extend(['--customizer', str(guiargs.customizer)]) + + for k,v in vars(parse_cli(cliargs)).items(): + if k not in vars(guiargs): + setattr(guiargs, k, v) + elif type(v) is dict: # use same settings for every player + players = guiargs.multi if len(v) == 0 else len(v) + setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, players + 1)}) + if guiargs.multi == 1 and guiargs.names.endswith("=="): + # allow settings code thru player names entry + guiargs.code[1] = guiargs.names + guiargs.names = "" + argsDump = vars(guiargs) + + needEnemizer = False + falsey = ["none", "default", False, 0] + for enemizerOption in ["shuffleenemies", "enemy_damage", "shufflebosses", "enemy_health"]: + if enemizerOption in argsDump: + if isinstance(argsDump[enemizerOption], dict): + for playerID,playerSetting in argsDump[enemizerOption].items(): + if not playerSetting in falsey: + needEnemizer = True + elif not argsDump[enemizerOption] in falsey: + needEnemizer = True + seeds = [] + if guiargs.count is not None and guiargs.seed: seed = guiargs.seed for _ in range(guiargs.count):