Merge branch 'OverworldShuffleDev' into OverworldShuffle

This commit is contained in:
codemann8
2025-11-08 11:18:52 -06:00
10 changed files with 99 additions and 59 deletions

View File

@@ -3274,16 +3274,20 @@ class Spoiler(object):
custom = self.metadata['custom_goals'][player] custom = self.metadata['custom_goals'][player]
if custom['gtentry'] and 'requirements' in custom['gtentry']: if custom['gtentry'] and 'requirements' in custom['gtentry']:
outfile.write('GT Entry Requirement:'.ljust(line_width) + 'custom\n') outfile.write('GT Entry Requirement:'.ljust(line_width) + 'custom\n')
outfile.write(' %s\n' % custom['gtentry']['goaltext'])
else: else:
outfile.write('GT Entry Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_gt_orig[player])) 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']: if custom['ganongoal'] and 'requirements' in custom['ganongoal']:
outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n') outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n')
outfile.write(' %s\n' % custom['ganongoal']['goaltext'])
else: else:
outfile.write('Ganon Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_ganon_orig[player])) 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']: if custom['pedgoal'] and 'requirements' in custom['pedgoal']:
outfile.write('Pedestal Requirement:'.ljust(line_width) + 'custom\n') outfile.write('Pedestal Requirement:'.ljust(line_width) + 'custom\n')
outfile.write(' %s\n' % custom['pedgoal']['goaltext'])
if custom['murahgoal'] and 'requirements' in custom['murahgoal']: if custom['murahgoal'] and 'requirements' in custom['murahgoal']:
outfile.write('Murahdahla Requirement:'.ljust(line_width) + 'custom\n') 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('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player])
outfile.write('\n') outfile.write('\n')
outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) 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) + ')') player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')')
goal = self.world.custom_goals[player]['gtentry'] goal = self.world.custom_goals[player]['gtentry']
if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: 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': 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]))) 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'] goal = self.world.custom_goals[player]['ganongoal']
if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: 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': 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]))) 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'] # goal = self.world.custom_goals[player]['pedgoal']
if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: # 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']) # outfile.write(str('Pedestal Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext'])
goal = self.world.custom_goals[player]['murahgoal'] # goal = self.world.custom_goals[player]['murahgoal']
if goal and 'requirements' in goal and goal['requirements'][0]['condition'] != 0x00: # 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(str('Murahdahla Sign Text' + player_name + ':').ljust(line_width) + '%s\n' % goal['goaltext'])
outfile.write('\n\nPrizes:\n\n') outfile.write('\n\nPrizes:\n\n')
for dungeon, prize in self.prizes.items(): for dungeon, prize in self.prizes.items():
outfile.write(str(dungeon + ':').ljust(line_width) + '%s\n' % prize) outfile.write(str(dungeon + ':').ljust(line_width) + '%s\n' % prize)

View File

@@ -1,5 +1,11 @@
# Changelog # 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 ## 0.6.1.1
- Fixed issue with Bosses goals in Custom Goal Framework - Fixed issue with Bosses goals in Custom Goal Framework
- Fixed error when using Custom Goals with no extra values - Fixed error when using Custom Goals with no extra values

14
Main.py
View File

@@ -645,23 +645,23 @@ def resolve_random_settings(world, args):
else: else:
raise Exception(f'Invalid {list(r.keys())[0]} requirement target for {goal_type}') raise Exception(f'Invalid {list(r.keys())[0]} requirement target for {goal_type}')
if req['condition'] & 0x7F == req_table['Pendants']: 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']: 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']: 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']: 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']: 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']: elif req['condition'] & 0x7F == req_table['Aga1']:
goal['logic']['aga1'] = True goal['logic']['aga1'] = True
elif req['condition'] & 0x7F == req_table['Aga2']: elif req['condition'] & 0x7F == req_table['Aga2']:
goal['logic']['aga2'] = True goal['logic']['aga2'] = True
elif req['condition'] & 0x7F == req_table['TriforcePieces']: 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']: 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) goal['requirements'].append(req)
except KeyError: except KeyError:
raise KeyError(f'Invalid {goal_type} requirement: {r}') raise KeyError(f'Invalid {goal_type} requirement: {r}')

View File

@@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType
from OverworldGlitchRules import create_owg_connections from OverworldGlitchRules import create_owg_connections
from Utils import bidict from Utils import bidict
version_number = '0.6.1.1' version_number = '0.6.1.2'
# branch indicator is intentionally different across branches # branch indicator is intentionally different across branches
version_branch = '' version_branch = ''

36
Rom.py
View File

@@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '72c4b2d00057d1faced32871d8081f3a' RANDOMIZERBASEHASH = '2039c11b935d3b81f78810d9f4be19d6'
class JsonRom(object): class JsonRom(object):
@@ -1270,8 +1270,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
goal_bytes += [req['target']] goal_bytes += [req['target']]
else: else:
goal_bytes += int16_as_bytes(req['target']) goal_bytes += int16_as_bytes(req['target'])
elif 'target' in req: elif req['condition'] & 0x80 == 0:
if req['condition'] & 0x7F < 0x08: 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']] goal_bytes += [req['target']]
else: else:
goal_bytes += int16_as_bytes(req['target']) 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: if start_address > 0x81D8:
raise Exception("Custom Goal data too long to fit in allocated space, try reducing the amount of requirements.") raise Exception("Custom Goal data too long to fit in allocated space, try reducing the amount of requirements.")
# gt entry # goal cutscene gfx
gtentry = world.custom_goals[player]['gtentry'] goals = {
if gtentry and 'cutscene_gfx' in gtentry: #goal: gfx addr, palette addr
gfx = gtentry['cutscene_gfx'] 'gtentry': (0x3081D8, 0x3081E6),
write_int16(rom, snes_to_pc(0x3081D8), gfx[0]) 'pedgoal': (0x3081ED, 0x3081F3),
rom.write_byte(snes_to_pc(0x3081E6), gfx[1]) '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 # 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' 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, seedstring = rom_header.split('|', 1)
gen = f'{gen:<3}' gen = f'{gen:<3}'
seedstring = f'{int(seedstring):09}' if seedstring.isdigit() else seedstring[:9] 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: elif len(rom_header) <= 9:
seedstring = f'{int(rom_header):09}' if rom_header.isdigit() else rom_header 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] 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): def get_custom_goal_text(type):
goal_text = world.custom_goals[player][type]['goaltext'] goal_text = world.custom_goals[player][type]['goaltext']
if '%d' in goal_text: placeholder_count = goal_text.count('%d')
return goal_text % world.custom_goals[player][type]['requirements'][0]['target'] 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 return goal_text
if world.custom_goals[player]['gtentry'] and 'goaltext' in world.custom_goals[player]['gtentry']: if world.custom_goals[player]['gtentry'] and 'goaltext' in world.custom_goals[player]['gtentry']:

View File

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

View File

@@ -1782,6 +1782,8 @@ class TextTable(object):
text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^") 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['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" 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" " ~~~2023~~~\nEriror\n\n"
" ~~~2022~~~\nAndy\n\n" " ~~~2022~~~\nAndy\n\n"
" ~~~2021~~~\nprdwong") " ~~~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_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['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" 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" " ~~~2022~~~\nObscure\n\n"
" ~~~2021~~~\nDaaanty\n\n" " ~~~2021~~~\nDaaanty\n\n"
" ~~~2019~~~\nJet082\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!") text['ganon_phase_3_alt'] = CompressedTextMapper.convert("Got wax in your ears? I cannot die!")
# 190 # 190
text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("Glitched\ntournament\nwinners\n{HARP}\n" text['sign_east_death_mountain_bridge'] = CompressedTextMapper.convert("Glitched\ntournament\nwinners\n{HARP}\n"
"~~~HMG 2025~~~\nSkele\n"
"~~~No Logic 2024~~~\ntam\n\n" "~~~No Logic 2024~~~\ntam\n\n"
"~~~HMG 2023~~~\ntam\n\n" "~~~HMG 2023~~~\ntam\n\n"
"~~~No Logic 2022~~~\nChexhuman\n\n" "~~~No Logic 2022~~~\nChexhuman\n\n"

Binary file not shown.

View File

@@ -577,9 +577,13 @@ class CustomSettings(object):
def load_yaml(path): def load_yaml(path):
if os.path.exists(Path(path)): try:
with open(path, "r", encoding="utf-8") as f: if os.path.exists(Path(path)):
return yaml.load(f, Loader=yaml.SafeLoader) with open(path, "r", encoding="utf-8") as f:
elif urllib.parse.urlparse(path).scheme in ['http', 'https']: return yaml.load(f, Loader=yaml.SafeLoader)
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) 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

View File

@@ -77,32 +77,37 @@ def bottom_frame(self, parent, args=None):
argsDump['sprite'] = argsDump['sprite'].name argsDump['sprite'] = argsDump['sprite'].name
save_settings(parent, argsDump, "last.json") 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: 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: if guiargs.count is not None and guiargs.seed:
seed = guiargs.seed seed = guiargs.seed
for _ in range(guiargs.count): for _ in range(guiargs.count):