Implemented Custom Goal Framework

This commit is contained in:
codemann8
2025-10-29 00:20:41 -05:00
parent ba444f4bbc
commit fdbe9cf9fd
9 changed files with 629 additions and 100 deletions

View File

@@ -187,6 +187,7 @@ class World(object):
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
set_player_attr('prizes', {'dig;': [], 'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0, 'enemies': []})
set_player_attr('default_zelda_region', 'Hyrule Dungeon Cellblock')
set_player_attr('custom_goals', {'gtentry': None, 'ganongoal': None, 'pedgoal': None, 'murahgoal': None})
set_player_attr('exp_cache', defaultdict(dict))
set_player_attr('enabled_entrances', {})
@@ -1211,16 +1212,35 @@ class CollectionState(object):
def item_count(self, item, player):
return self.prog_items[item, player]
def everything(self, player):
all_locations = self.world.get_filled_locations(player)
all_locations.remove(self.world.get_location('Ganon', player))
return (len([x for x in self.locations_checked if x.player == player])
def everything(self, player, all_except=0):
all_locations = [x for x in self.world.get_filled_locations(player) if not x.locked]
return (len([x for x in self.locations_checked if x.player == player and not x.locked]) + all_except
>= len(all_locations))
def has_crystals(self, count, player):
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count
def has_pendants(self, count, player):
pendants = ['Green Pendant', 'Red Pendant', 'Blue Pendant']
return len([pendant for pendant in pendants if self.has(pendant, player)]) >= count
def has_bosses(self, count, player, prize_type=None):
dungeons = 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', "Thieves' Town", 'Skull Woods', 'Ice Palace', 'Misery Mire', 'Turtle Rock'
reachable_bosses = 0
for d in dungeons:
region = self.world.get_region(f'{d} - Boss Kill', player)
if prize_type is None or prize_type in region.dungeon.prize.name:
if self.can_reach(region, None, player):
reachable_bosses += 1
return reachable_bosses >= count
def has_crystal_bosses(self, count, player):
return self.has_bosses(count, player, 'Crystal')
def has_pendant_bosses(self, count, player):
return self.has_bosses(count, player, 'Pendant')
def can_lift_rocks(self, player):
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
@@ -3015,6 +3035,7 @@ class Spoiler(object):
'flute_mode': self.world.flute_mode,
'bow_mode': self.world.bow_mode,
'goal': self.world.goal,
'custom_goals': self.world.custom_goals,
'ow_shuffle': self.world.owShuffle,
'ow_terrain': self.world.owTerrain,
'ow_crossed': self.world.owCrossed,
@@ -3250,8 +3271,19 @@ class Spoiler(object):
if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']:
outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player])
outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player])
outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player]))
outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player]))
custom = self.metadata['custom_goals'][player]
if 'requirements' in custom['gtentry']:
outfile.write('GT Entry Requirement:'.ljust(line_width) + 'custom\n')
else:
outfile.write('GT Entry Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_gt_orig[player]))
if 'requirements' in custom['ganongoal']:
outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n')
else:
outfile.write('Ganon Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_ganon_orig[player]))
if 'requirements' in custom['pedgoal']:
outfile.write('Pedestal Requirement:'.ljust(line_width) + 'custom\n')
if 'requirements' in custom['murahgoal']:
outfile.write('Murahdahla Requirement:'.ljust(line_width) + 'custom\n')
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])

View File

@@ -228,12 +228,17 @@ def generate_itempool(world, player):
if world.timer in ['ohko', 'timed-ohko']:
world.can_take_damage = False
if world.goal[player] in ['pedestal', 'triforcehunt']:
goal_req = None
if world.custom_goals[player]['ganongoal'] and 'requirements' in world.custom_goals[player]['ganongoal']:
goal_req = world.custom_goals[player]['ganongoal']['requirements'][0]
if world.goal[player] in ['pedestal', 'triforcehunt'] or (goal_req and goal_req['condition'] == 0x00):
set_event_item(world, player, 'Ganon', 'Nothing')
else:
set_event_item(world, player, 'Ganon', 'Triforce')
if world.goal[player] in ['triforcehunt', 'trinity']:
if world.custom_goals[player]['murahgoal'] and 'requirements' in world.custom_goals[player]['murahgoal']:
goal_req = world.custom_goals[player]['murahgoal']['requirements'][0]
if world.goal[player] in ['triforcehunt', 'trinity'] or (goal_req and goal_req['condition'] != 0x00):
region = world.get_region('Hyrule Castle Courtyard', player)
loc = Location(player, "Murahdahla", parent=region)
region.locations.append(loc)
@@ -1159,11 +1164,17 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
place_item('Link\'s Uncle', swords_to_use.pop())
place_item('Blacksmith', swords_to_use.pop())
place_item('Pyramid Fairy - Left', swords_to_use.pop())
if goal not in ['pedestal', 'trinity']:
place_item('Master Sword Pedestal', swords_to_use.pop())
else:
place_item('Master Sword Pedestal', 'Triforce')
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00:
place_item('Master Sword Pedestal', 'Nothing')
world.get_location('Master Sword Pedestal', player).locked = True
pool.append(swords_to_use.pop())
else:
if goal not in ['pedestal', 'trinity']:
place_item('Master Sword Pedestal', swords_to_use.pop())
else:
place_item('Master Sword Pedestal', 'Triforce')
world.get_location('Master Sword Pedestal', player).locked = True
pool.append(swords_to_use.pop())
else:
pool.extend(diff.progressivesword if want_progressives() else diff.basicsword)
if swords == 'assured':
@@ -1189,8 +1200,12 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
# note: massage item pool now handles shrinking the pool appropriately
if goal in ['pedestal', 'trinity'] and swords != 'vanilla':
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00:
place_item('Master Sword Pedestal', 'Nothing')
world.get_location('Master Sword Pedestal', player).locked = True
elif goal in ['pedestal', 'trinity'] and swords != 'vanilla':
place_item('Master Sword Pedestal', 'Triforce')
world.get_location('Master Sword Pedestal', player).locked = True
if world.bow_mode[player].startswith('retro'):
pool = [item.replace('Single Arrow', 'Rupees (5)') for item in pool]
pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool]
@@ -1352,8 +1367,12 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
elif timer == 'ohko':
clock_mode = 'ohko'
if goal in ['pedestal', 'trinity']:
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00:
place_item('Master Sword Pedestal', 'Nothing')
world.get_location('Master Sword Pedestal', player).locked = True
elif goal in ['pedestal', 'trinity']:
place_item('Master Sword Pedestal', 'Triforce')
world.get_location('Master Sword Pedestal', player).locked = True
if mode == 'standard':
if world.keyshuffle[player] == 'universal':
@@ -1466,8 +1485,12 @@ def make_customizer_pool(world, player):
elif timer == 'ohko':
clock_mode = 'ohko'
if world.goal[player] in ['pedestal', 'trinity']:
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal'] and world.custom_goals[player]['pedgoal']['requirements'][0]['condition'] == 0x00:
place_item('Master Sword Pedestal', 'Nothing')
world.get_location('Master Sword Pedestal', player).locked = True
elif world.goal[player] in ['pedestal', 'trinity']:
place_item('Master Sword Pedestal', 'Triforce')
world.get_location('Master Sword Pedestal', player).locked = True
guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl']
if world.is_tile_swapped(0x18, player) or world.flute_mode[player] == 'active':

153
Main.py
View File

@@ -521,8 +521,159 @@ def resolve_random_settings(world, args):
else:
# this will be handled in ItemList.py and custom item pool is used to determine the numbers
world.treasure_hunt_count[p], world.treasure_hunt_total[p] = 0, 0
if world.customizer:
def process_goal(goal_type):
goal_input = goals[player][goal_type]
world.custom_goals[player][goal_type] = goal = {}
if 'cutscene_gfx' in goal_input and goal_type in ['gtentry', 'pedgoal', 'murahgoal']:
gfx = goal_input['cutscene_gfx']
if type(gfx) is str:
from Tables import item_gfx_table
if gfx.lower() == 'random':
gfx = random.choice(list(item_gfx_table.keys()))
if gfx in item_gfx_table:
goal['cutscene_gfx'] = (item_gfx_table[gfx][1] + (0x8000 if not item_gfx_table[gfx][0] else 0), item_gfx_table[gfx][2])
else:
raise Exception(f'Invalid name "{gfx}" in customized {goal_type} cutscene gfx')
else:
goal['cutscene_gfx'] = gfx
if 'requirements' in goal_input:
if goal_type == 'ganongoal' and world.goal[player] == 'pedestal':
goal['requirements'] = [0x00]
goal['logic'] = False
return
goal['requirements'] = []
goal['logic'] = {}
if 'goaltext' in goal_input:
goal['goaltext'] = goal_input['goaltext']
else:
raise Exception(f'Missing goal text for {goal_type}')
req_table = {
'Invulnerable': 0x00,
'Pendants': 0x01,
'Crystals': 0x02,
'PendantBosses': 0x03,
'CrystalBosses': 0x04,
'Bosses': 0x05,
'Agahnim1Defeated': 0x06,
'Agahnim1': 0x06,
'Aga1': 0x06,
'Agahnim2Defeated': 0x07,
'Agahnim2': 0x07,
'Aga2': 0x07,
'GoalItemsCollected': 0x08,
'GoalItems': 0x08,
'TriforcePieces': 0x08,
'TriforceHunt': 0x08,
'MaxCollectionRate': 0x09,
'CollectionRate': 0x09,
'Collection': 0x09,
'CustomGoal': 0x0A,
'Custom': 0x0A,
}
if isinstance(goal_input['requirements'], list):
for r in list(goal_input['requirements']):
req = {}
try:
req['condition'] = req_table[list(r.keys())[0]]
if req['condition'] == req_table['Invulnerable']:
goal['requirements']= [req]
goal['logic'] = False
break
elif req['condition'] == req_table['CustomGoal']:
if isinstance(r['address'], int) and 0x7E0000 <= r['address'] <= 0x7FFFFF:
compare_table = {
'minimum': 0x00,
'at least': 0x00,
'equal': 0x01,
'equals': 0x01,
'equal to': 0x01,
'any flag': 0x02,
'all flags': 0x03,
'flags match': 0x03,
'count bits': 0x04,
'count flags': 0x04,
}
if r['comparison'] in compare_table:
options = compare_table[r['comparison']]
if r['address'] >= 0x7F0000:
options |= 0x10
if isinstance(r['target'], int) and 0 <= r['target'] <= 0xFFFF:
if 'size' in r and r['size'] in ['word', '16-bit', '16bit', '16 bit', '16', '2-byte', '2byte', '2 byte', '2-bytes', '2 bytes']:
options |= 0x08
req['target'] = r['target']
elif 0 <= r['target'] <= 0xFF:
req['target'] = r['target']
else:
raise Exception(f'Invalid custom goal target for {goal_type}, must be an 8-bit integer')
req.update({'address': r['address'] & 0xFFFF, 'options': options})
goal['requirements'].append(req)
else:
raise Exception(f'Invalid custom goal target for {goal_type}, must be a 16-bit integer')
else:
raise KeyError(f'Invalid custom goal comparison for {goal_type}')
else:
raise Exception(f'Custom goal address for {goal_type} only allows 0x7Exxxx and 0x7Fxxxx addresses')
else:
if req['condition'] not in [req_table['Aga1'], req_table['Aga2']]:
if 'target' not in r:
req['condition'] |= 0x80
else:
if isinstance(r['target'], int):
if req['condition'] < req_table['TriforcePieces']:
if 0 <= r['target'] <= 0xFF:
req['target'] = r['target']
else:
raise Exception(f'Invalid {list(r.keys())[0]} requirement target for {goal_type}, must be an 8-bit integer')
else:
if 0 <= r['target'] <= 0xFFFF:
req['target'] = r['target']
else:
raise Exception(f'Invalid {list(r.keys())[0]} requirement target for {goal_type}, must be a 16-bit integer')
elif isinstance(r['target'], str):
if r['target'].lower() == 'random':
req['target'] = 'random'
elif r['target'].endswith('%') and 1 <= int(r['target'][:-1]) <= 100:
req['target'] = req['target']
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
elif req['condition'] & 0x7F == req_table['Crystals']:
goal['logic']['crystals'] = req['target'] or 7
elif req['condition'] & 0x7F == req_table['PendantBosses']:
goal['logic']['pendant_bosses'] = req['target'] or 3
elif req['condition'] & 0x7F == req_table['CrystalBosses']:
goal['logic']['crystal_bosses'] = req['target'] or 7
elif req['condition'] & 0x7F == req_table['Bosses']:
goal['logic']['bosses'] = req['target'] or 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
elif req['condition'] & 0x7F == req_table['CollectionRate']:
goal['logic']['collection'] = req['target'] or None
goal['requirements'].append(req)
except KeyError:
raise KeyError(f'Invalid {goal_type} requirement: {r}')
else:
raise KeyError(f'Invalid {goal_type} requirement definition')
if 'logic' in goal_input and goal['logic'] is not None:
goal['logic'].update(goal_input['logic'])
return
goals = world.customizer.get_goals()
for player in range(1, world.players + 1):
if goals and player in goals:
for g in ['gtentry', 'ganongoal', 'pedgoal', 'murahgoal']:
if g in goals[player]:
process_goal(g)
return
def set_starting_inventory(world, args):
for player in range(1, world.players + 1):

152
Rom.py
View File

@@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'e20f407ef55da945f893d32ee6fc541d'
RANDOMIZERBASEHASH = 'b7817fb00fb0a918a7fa275ff8f4c3be'
class JsonRom(object):
@@ -1218,8 +1218,6 @@ def patch_rom(world, rom, player, team, is_mystery=False):
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
if world.is_pyramid_open(player):
rom.initial_sram.pre_open_pyramid_hole()
if world.crystals_needed_for_gt[player] == 0:
rom.initial_sram.pre_open_ganons_tower()
rom.write_byte(0x18008F, 0x01 if world.is_atgt_swapped(player) else 0x00) # AT/GT swapped
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
@@ -1247,22 +1245,105 @@ def patch_rom(world, rom, player, team, is_mystery=False):
(0x02 if 'bombs' in world.escape_assist[player] else 0x00) |
(0x04 if 'magic' in world.escape_assist[player] else 0x00))) # Escape assist
if world.goal[player] in ['pedestal', 'triforcehunt']:
rom.write_byte(0x1801A8, 0x01) # make ganon invincible
elif world.goal[player] in ['dungeons']:
rom.write_byte(0x1801A8, 0x02) # make ganon invincible until all dungeons are beat
elif world.goal[player] in ['crystals', 'trinity']:
rom.write_byte(0x1801A8, 0x04) # make ganon invincible until all crystals
elif world.goal[player] in ['ganonhunt']:
rom.write_byte(0x1801A8, 0x05) # make ganon invincible until all triforce pieces collected
elif world.goal[player] in ['completionist']:
rom.write_byte(0x1801A8, 0x0B) # make ganon invincible until everything is collected
else:
rom.write_byte(0x1801A8, 0x03) # make ganon invincible until all crystals and aga 2 are collected
gt_entry, ped_pull, ganon_goal, murah_goal = [], [], [], []
# 00: Invulnerable
# 01: All pendants
# 02: All crystals
# 03: Pendant bosses
# 04: Crystal bosses
# 05: Prize bosses
# 06: Agahnim 1 defeated
# 07: Agahnim 2 defeated
# 08: Goal items collected (ie. Triforce Pieces)
# 09: Max collection rate
# 0A: Custom goal
rom.write_byte(0x18019A, world.crystals_needed_for_gt[player])
rom.write_byte(0x1801A6, world.crystals_needed_for_ganon[player])
rom.write_byte(0x1801A2, 0x00) # ped requirement is vanilla, set to 0x1 for special requirements
def get_goal_bytes(type):
goal_bytes = []
for req in world.custom_goals[player][type]['requirements']:
goal_bytes += [req['condition']]
if req['condition'] == 0x0A:
# custom goal
goal_bytes += [req['options']]
goal_bytes += int16_as_bytes(req['address'])
if 0x08 & req['options'] == 0:
goal_bytes += [req['target']]
else:
goal_bytes += int16_as_bytes(req['target'])
elif 'target' in req:
if req['condition'] & 0x7F < 0x08:
goal_bytes += [req['target']]
else:
goal_bytes += int16_as_bytes(req['target'])
return goal_bytes
if world.custom_goals[player]['gtentry'] and 'requirements' in world.custom_goals[player]['gtentry']:
gt_entry += get_goal_bytes('gtentry')
else:
gt_entry += [0x02, world.crystals_needed_for_gt[player]]
if len(gt_entry) == 0 or gt_entry == [0x02, 0x00]:
rom.initial_sram.pre_open_ganons_tower()
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal']:
ped_pull += get_goal_bytes('pedgoal')
else:
ped_pull += [0x81]
if world.custom_goals[player]['murahgoal'] and 'requirements' in world.custom_goals[player]['murahgoal']:
murah_goal += get_goal_bytes('murahgoal')
else:
if world.goal[player] in ['triforcehunt', 'trinity']:
murah_goal += [0x88]
else:
murah_goal += [0x00]
if world.custom_goals[player]['ganongoal'] and 'requirements' in world.custom_goals[player]['ganongoal']:
ganon_goal += get_goal_bytes('ganongoal')
else:
if world.goal[player] in ['pedestal', 'triforcehunt']:
ganon_goal = [0x00]
elif world.goal[player] in ['dungeons']:
ganon_goal += [0x81, 0x82, 0x06, 0x07] # pendants, crystals, and agas
elif world.goal[player] in ['crystals', 'trinity']:
ganon_goal += [0x02, world.crystals_needed_for_ganon[player]]
elif world.goal[player] in ['ganonhunt']:
ganon_goal += [0x88] # triforce pieces
elif world.goal[player] in ['completionist']:
ganon_goal += [0x81, 0x82, 0x06, 0x07, 0x89] # AD and max collection rate
else:
ganon_goal += [0x02, world.crystals_needed_for_ganon[player], 0x07] # crystals and aga2
gt_entry += [0xFF]
ped_pull += [0xFF]
ganon_goal += [0xFF]
murah_goal += [0xFF]
start_address = 0x8198 + 8
write_int16(rom, 0x180198, start_address)
rom.write_bytes(snes_to_pc(0xB00000 + start_address), gt_entry)
start_address += len(gt_entry)
write_int16(rom, 0x18019A, start_address)
rom.write_bytes(snes_to_pc(0xB00000 + start_address), ganon_goal)
start_address += len(ganon_goal)
write_int16(rom, 0x18019C, start_address)
rom.write_bytes(snes_to_pc(0xB00000 + start_address), ped_pull)
start_address += len(ped_pull)
write_int16(rom, 0x18019E, start_address)
rom.write_bytes(snes_to_pc(0xB00000 + start_address), murah_goal)
start_address += len(murah_goal)
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])
# 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'
@@ -1654,26 +1735,6 @@ def patch_rom(world, rom, player, team, is_mystery=False):
write_enemizer_tweaks(rom, world, player)
write_strings(rom, world, player, team)
# gt entry
if world.customizer:
gtentry = world.customizer.get_gtentry()
if gtentry and player in gtentry:
gtentry = gtentry[player]
if 'cutscene_gfx' in gtentry:
gfx = gtentry['cutscene_gfx']
if type(gfx) is str:
from Tables import item_gfx_table
if gfx.lower() == 'random':
gfx = random.choice(list(item_gfx_table.keys()))
if gfx in item_gfx_table:
write_int16(rom, snes_to_pc(0x3081AA), item_gfx_table[gfx][1] + (0x8000 if not item_gfx_table[gfx][0] else 0))
rom.write_byte(snes_to_pc(0x3081AC), item_gfx_table[gfx][2])
else:
logging.getLogger('').warning('Invalid name "%s" in customized GT entry cutscene gfx', gfx)
else:
write_int16(rom, snes_to_pc(0x3081AA), gfx[0])
rom.write_byte(snes_to_pc(0x3081AC), gfx[1])
# write initial sram
rom.write_initial_sram()
@@ -2502,6 +2563,21 @@ def write_strings(rom, world, player, team):
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
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']
return goal_text
if world.custom_goals[player]['gtentry'] and 'goaltext' in world.custom_goals[player]['gtentry']:
tt['sign_ganons_tower'] = get_custom_goal_text('gtentry')
if world.custom_goals[player]['ganongoal'] and 'goaltext' in world.custom_goals[player]['ganongoal']:
tt['sign_ganon'] = get_custom_goal_text('ganongoal')
if world.custom_goals[player]['pedgoal'] and 'goaltext' in world.custom_goals[player]['pedgoal']:
tt['mastersword_pedestal_goal'] = get_custom_goal_text('pedgoal')
if world.custom_goals[player]['murahgoal'] and 'goaltext' in world.custom_goals[player]['murahgoal']:
tt['murahdahla'] = get_custom_goal_text('murahgoal')
tt['kakariko_tavern_fisherman'] = TavernMan_texts[random.randint(0, len(TavernMan_texts) - 1)]

180
Rules.py
View File

@@ -61,25 +61,33 @@ def set_rules(world, player):
drop_rules(world, player)
challenge_room_rules(world, player)
if world.goal[player] == 'dungeons':
# require all dungeons to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has_beaten_aga(player) and state.has('Beat Agahnim 2', player) and state.has('Beat Boss', player, 10))
elif world.goal[player] in ['crystals', 'ganon']:
add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player))
if world.goal[player] == 'ganon':
# require aga2 to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
elif world.goal[player] in ['triforcehunt', 'trinity']:
if world.goal[player] == 'trinity':
if world.custom_goals[player]['ganongoal'] and 'requirements' in world.custom_goals[player]['ganongoal']:
rule = get_goal_rule('ganongoal', world, player)
add_rule(world.get_location('Ganon', player), rule)
else:
if world.goal[player] == 'dungeons':
# require all dungeons to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has_beaten_aga(player) and state.has('Beat Agahnim 2', player) and state.has('Beat Boss', player, 10))
elif world.goal[player] in ['crystals', 'ganon']:
add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player))
for location in world.get_region('Hyrule Castle Courtyard', player).locations:
if location.name == 'Murahdahla':
add_rule(location, lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'ganonhunt':
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'completionist':
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))
if world.goal[player] == 'ganon':
# require aga2 to beat ganon
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
elif world.goal[player] in ['triforcehunt', 'trinity']:
if world.goal[player] == 'trinity':
add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player))
elif world.goal[player] == 'ganonhunt':
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'completionist':
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))
for location in world.get_region('Hyrule Castle Courtyard', player).locations:
if location.name == 'Murahdahla':
if world.custom_goals[player]['murahgoal'] and 'requirements' in world.custom_goals[player]['murahgoal']:
rule = get_goal_rule('murahgoal', world, player)
add_rule(location, rule)
else:
add_rule(location, lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
if (world.flute_mode[player] != 'active' and not world.is_tile_swapped(0x18, player)
and 'Ocarina (Activated)' not in list(map(str, [i for i in world.precollected_items if i.player == player]))):
@@ -175,6 +183,132 @@ def add_rule(spot, rule, combine='and'):
else:
spot.access_rule = lambda state: rule(state) and old_rule(state)
def get_goal_rule(goal_type, world, player):
goal_data = world.custom_goals[player][goal_type]
if goal_data['requirements'][0]['condition'] == 0x00:
return lambda state: False
rule = None
def add_to_rule(new_rule):
nonlocal rule
if rule is None:
rule = new_rule
else:
rule = and_rule(rule, new_rule)
if 'logic' in goal_data:
for logic, data in goal_data['logic'].items():
if logic == 'pendants':
pendants = int(data)
add_to_rule(lambda state: state.has_pendants(pendants, player))
elif logic == 'crystals':
crystals = int(data)
add_to_rule(lambda state: state.has_crystals(crystals, player))
elif logic == 'pendant_bosses':
pendant_bosses = int(data)
add_to_rule(lambda state: state.has_pendant_bosses(pendant_bosses, player))
elif logic == 'crystal_bosses':
crystal_bosses = int(data)
add_to_rule(lambda state: state.has_crystal_bosses(crystal_bosses, player))
elif logic == 'bosses':
bosses = int(data)
add_to_rule(lambda state: state.has('Beat Boss', player, bosses))
elif logic == 'aga1':
add_to_rule(lambda state: state.has('Beat Agahnim 1', player))
elif logic == 'aga2':
add_to_rule(lambda state: state.has('Beat Agahnim 2', player))
elif logic == 'goal_items':
if data is not None:
goal_items = int(data)
add_to_rule(lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= goal_items)
else:
add_to_rule(lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif logic == 'collection':
if data is not None:
all_locations = [x for x in world.get_filled_locations(player) if not x.locked]
collection = int(data) - len(all_locations)
add_to_rule(lambda state: state.everything(player, collection))
else:
add_to_rule(lambda state: state.everything(player))
elif logic == 'item':
for item in data:
item_name = item
if '(' in item_name:
item_name, region_name = item_name.rsplit(' (', 1)
region_name = region_name.rstrip(')')
region = world.get_region(region_name, player)
if region and region.dungeon:
region_name = region.dungeon.name
else:
try:
if world.get_dungeon(region_name, player):
pass
except:
raise Exception(f'Invalid dungeon/region name in custom goal logic for item {item}')
item_name = f'{item_name} ({region_name})'
if '=' in item_name:
item_name, count = item_name.rsplit('=', 1)
count = int(count)
add_to_rule(lambda state: state.has(item_name, player, count))
else:
add_to_rule(lambda state: state.has(item_name, player))
elif logic == 'access':
for region_name in data:
region = world.get_region(region_name, player)
if not region:
raise Exception(f'Invalid region name in custom goal logic for region: {region_name}')
add_to_rule(lambda state: state.can_reach(region, None, player))
elif logic == 'ability':
for ability in data:
param = None
if '(' in ability:
ability, param = ability.split('(', 1)
param = param.rstrip(')')
if ability == 'FarmBombs':
add_to_rule(lambda state: state.can_farm_bombs(player))
elif ability == 'FarmRupees':
add_to_rule(lambda state: state.can_farm_rupees(player))
elif ability == 'NoBunny':
if not param:
raise Exception(f'NoBunny ability requires a region argument in custom goal logic')
bunny_region = param
region = world.get_region(bunny_region, player)
if region:
add_to_rule(lambda state: state.is_not_bunny(bunny_region, player))
else:
raise Exception(f'Invalid region name in custom goal logic for NoBunny ability: {param}')
elif ability == 'CanUseBombs':
add_to_rule(lambda state: state.can_use_bombs(player))
elif ability == 'CanBonkDrop':
add_to_rule(lambda state: state.can_collect_bonkdrops(player))
elif ability == 'CanLift':
add_to_rule(lambda state: state.can_lift_rocks(player))
elif ability == 'MagicExtension':
magic_count = 16
if param:
magic_count = int(param)
add_to_rule(lambda state: state.can_extend_magic(player, magic_count))
elif ability == 'CanStun':
add_to_rule(lambda state: state.can_stun_enemies(player))
elif ability == 'CanKill':
if param:
enemy_count = int(param)
add_to_rule(lambda state: state.can_kill_most_things(player, enemy_count))
else:
add_to_rule(lambda state: state.can_kill_most_things(player))
elif ability == 'CanShootArrows':
add_to_rule(lambda state: state.can_shoot_arrows(player))
elif ability == 'CanFlute':
add_to_rule(lambda state: state.can_flute(player))
elif ability == 'HasFire':
add_to_rule(lambda state: state.has_fire_source(player))
elif ability == 'CanMelt':
add_to_rule(lambda state: state.can_melt_things(player))
elif ability == 'HasMMMedallion':
add_to_rule(lambda state: state.has_misery_mire_medallion(player))
elif ability == 'HasTRMedallion':
add_to_rule(lambda state: state.has_turtle_rock_medallion(player))
return rule if rule is not None else lambda state: True
def add_bunny_rule(spot, player):
if spot.can_cause_bunny(player):
add_rule(spot, lambda state: state.has_Pearl(player))
@@ -244,7 +378,11 @@ def global_rules(world, player):
set_rule(world.get_entrance('Flute Spot 8', player), lambda state: state.can_flute(player))
# overworld location rules
set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
if world.custom_goals[player]['pedgoal'] and 'requirements' in world.custom_goals[player]['pedgoal']:
rule = get_goal_rule('pedgoal', world, player)
set_rule(world.get_location('Master Sword Pedestal', player), rule)
else:
set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has_beam_sword(player))
set_rule(world.get_location('Old Man', player), lambda state: state.has('Return Old Man', player))
set_rule(world.get_location('Old Man Drop Off', player), lambda state: state.has('Escort Old Man', player))
@@ -412,9 +550,13 @@ def global_rules(world, player):
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!)
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Turtle Opened', player))
if world.custom_goals[player]['gtentry'] and 'requirements' in world.custom_goals[player]['gtentry']:
rule = get_goal_rule('gtentry', world, player)
set_rule(world.get_entrance('Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', player), rule)
else:
set_rule(world.get_entrance('Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player))
if not world.is_atgt_swapped(player):
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player))
set_rule(world.get_entrance('Ganons Tower' if not world.is_atgt_swapped(player) else 'Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player))
# Start of door rando rules
# TODO: Do these need to flag off when door rando is off? - some of them, yes

View File

@@ -2043,5 +2043,6 @@ class TextTable(object):
text['ganon_phase_3_no_silvers'] = CompressedTextMapper.convert("You can't best me without silver arrows!")
text['ganon_phase_3_silvers'] = CompressedTextMapper.convert("Oh no! Silver! My one true weakness!")
text['murahdahla'] = CompressedTextMapper.convert("Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n{PAUSE3}\n… … …\nWait! You can see me? I knew I should have\nhidden in a hollow tree.")
text['mastersword_pedestal_goal'] = CompressedTextMapper.convert("To claim thy reward, you must present all 3 Pendants of Virtue.")
text['end_pad_data'] = bytearray([0xfb])
text['terminator'] = bytearray([0xFF, 0xFF])

Binary file not shown.

View File

@@ -70,6 +70,134 @@ Then each player can have the entire item pool defined. The name of item should
Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon.
### goals
This must be defined by player. Each player number should be listed with the appropriate custom goals. This section has four primary subsections for each of the current supported events:
* `gtentry` (Ganon's Tower entrance)
* `ganongoal` (Ganon vulnerability)
* `pedpull` (Master Sword Pedestal activation)
* `murahgoal` (Murahdahla requirement, if given requirements, Murahdahla appears always and acts as an alternative way to beat the game)
These four custom goals use the following identical structure to define them. These goals have four primary subsections: `cutscene_gfx`, `goaltext`, `requirements`, and `logic`
#### cutscene_gfx
This is where you can define custom GFX to be used for an event that has an animation (currently only the GT entry cutscene is supported). For convenience, there are a number of pre-defined names that can be used to indicate already known GFX values built into the ROM. There are too many to list, but a full list can be found in `item_gfx_table` in `Tables.py`. You can also use `Random` and it will take a random one from the aforementioned table.
```yaml
goals:
1:
gtentry:
cutscene_gfx: Mirror Shield
```
Alternatively, you may also supply a custom address and palette ID, respectively, if you are injecting your own personal custom GFX into the ROM.
```yaml
goals:
1:
gtentry:
cutscene_gfx:
- 0x8140
- 0x04
```
#### goaltext
This is where you define the dialogue that will show in-game that informs the player what the goal is. This section is required if a goal event has any `requirements` defined. This value can contain `%d` as a numeric placeholder, where this value will be provided by the goal information provided in the `requirements` section (prioritizes the first goal defined).
A new dialogue has been added for Master Sword Pedestal, if you attempt to receive the pedestal item but do not satisfy the condition for it, a dialogue will appear.
#### requirements
For the various events, you may define many conditions for the player to meet. All of the conditions you specify must pass to activate the event. There are several built-in conditions that can be used and the logic will be automatically added for consideration. However, when `CustomGoal` goals are used, there is no automatic logic that gets applied for this; for this, you must supply additional logic information to be used during generation. For more information on this, see the `logic` subsection. Keep in mind, through this level of customization, it is possible to create unbeatable games, and it will be possible to require things that the game doesn't provide a way to see the current progress for, and none of the options here change any HUD or UI elements to expose that information. It will be important to be careful when making these definitions and to use all provided tools and information to minimize these risks. These are the current supported conditions:
* `Pendants` (Default: 3)
* `Crystals` (Default: 7)
* `PendantBosses` (Default: 3)
* `CrystalBosses` (Default: 7)
* `Bosses` (Default: 10)
* `Agahnim1Defeated`
* `Agahnim2Defeated`
* `TriforcePieces` (Default: set elsewhere)
* `CollectionRate` (Default: Max)
* `CustomGoal` (Needs additional `logic` defined when applicable)
These condition sections use a default target value unless a `target` is specified to override the default.
In addition, we have provided a `CustomGoal` to allow for very advanced control over custom requirements; this however requires some knowledge of either the rando assembly code, the LTTP disassembly source, or autotracker memory addresses. `CustomGoal` uses a few additional required and optional subsections:
* `address`: An address from memory that the game should read to compare (Only addresses from banks 0x7E and 0x7F are valid)
* `target`: A value to use to compare to the value found at the address (Hexadecimal values can be used)
* `size`: Optional, default is 1 byte will be read from memory (`16bit` or `2 bytes` are valid keywords to specify 2 bytes should be read and compared)
* `comparison`: This is to specify the method of comparison that should take place
##### comparison
* `minimum` or `at least`: Checks if a value is met or exceeded. This will likely be the most common comparison method.
* `equals`: Checks if a value exactly matches
* `any flag`: Checks against a bitfield value to see if any flag bits are set. (ie. a target of 0x40 only checks the one flag)
* `flags match`: Checks against a bitfield value to see if all flag bits are set. (ie. a target of 0x70 checks if all 3 bits are set)
* `count bits`: Counts the number of bits are set and compares that to the target
#### logic
Logic is automatically calculated for all of the basic out-of-the-box conditions, so nothing additional is required for specification for these. But for `CustomGoal`, this `logic` subsection is here to allow mode sculptors to provide this custom logic. This section is handled by three subsections: `item`, `access`, and `ability`
##### item
This is simply a list of items that are to be logically required to unlock the event. If multiple of the an item are needed, use an `=` followed by the amount required (ie. `- Triforce Piece=30`). For better support of Door Rando, dungeon items can be specified with a region name instead of a dungeon name; this way if you want to require a specific Big Key that opens a specific big chest in some room, this is possible to achieve (ie. `- Big Key (PoD Big Chest Balcony)`)
##### access
This is a list of regions that are logically required to be able to access before unlocking an event. Region names are internally named and not exposed on spoiler logs, but can be found by browsing `Regions.py`
##### ability
This is a list of abilities that the player logically requires. These are built-in logical patterns to make it easy to bundle larger nuanced requirements into one single definition. Some abilities take in an optional or required parameter, specified within `(` and `)` following the ability keyword. Here are the allowed keywords:
* `FarmBombs`: Link has access to repeatedly acquire bombs
* `CanUseBombs`: Link has the ability to use bombs
* `FarmRupees`: Link has access to repeatedly acquire rupees
* `NoBunny(<region name>)`: Link is required to not be a bunny in the specified region
* `MagicExtension(<number>)`: Link has a magic meter with a higher capacity (parameter is optional, a value of 8 represents one normal full magic bar, default value if left blank is 16, which is equivalent to half magic or one bottle with access to a green potion)
* `CanStun`: Link has the ability to stun enemies
* `CanKill(<number of enemies>)`: Link has the ability to kill most enemy types (parameter is optional, higher number tends to favor weapons that don't consume ammo, default value is 6 enemies)
* `CanShootArrows`: Link has ability to fire arrows at enemies
* `CanBonkDrop`: Link is able to retrieve Bonk Drops from trees and rocks
* `CanLift`: Link is able to lift basic rocks
* `CanFlute`: Link has ability to use flute (includes access to flute activation)
* `HasFire`: Link has a fire source
* `CanMelt`: Link can melt ice with Firerod or Bombos
* `HasMMMedallion`: Link has the medallion required for unlocking Misery Mire entrance
* `HasTRMedallion`: Link has the medallion required for unlocking Turtle Rock entrance
#### (example)
This entire section is very advanced and can be used to make very powerful customizations to the game. To make the overall definition more clear, we provide an example that makes use of a lot of the controls in place: Ganon requiring both 5 crystals AND requiring opening the GT Big Chest
```yaml
goals:
1:
ganongoal:
goaltext: Youll need %d crystals and to open the Big Chest in Ganons Tower
requirements:
- Crystals:
target: 5
- Custom:
address: 0x7ef118
target: 0x80
comparison: flags match
logic:
item:
- Big Key (GT Big Chest)
access:
- GT Big Chest
ability:
- NoBunny(GT Big Chest)
```
### placements
This must be defined by player. Each player number should be listed with the appropriate placement list.
@@ -348,27 +476,3 @@ prices:
Dark Death Mountain Shop - Right: 300
Dark Lake Hylia Shop - Left: 200
```
### gt_entry
This must be defined by player. This is where you are able to customize aspects of GT entry
#### cutscene_gfx
This is where you can define custom GFX to be used in the GT entry cutscene. For convenience, there are a number of pre-defined names that can be used to indicate already known GFX values built into the ROM. There are too many to list, but a full list can be found in `item_gfx_table` in `Tables.py`. You can also use `Random` and it will take a random one from the aforementioned table.
```
gt_entry:
1:
cutscene_gfx: Mirror Shield
```
Alternatively, you may also supply a custom address and palette ID, respectively, if you are injecting your own personal custom GFX into the ROM.
```
gt_entry:
1:
cutscene_gfx:
- 0x8140
- 0x04
```

View File

@@ -299,9 +299,9 @@ class CustomSettings(object):
return self.file_source['enemies']
return None
def get_gtentry(self):
if 'gt_entry' in self.file_source:
return self.file_source['gt_entry']
def get_goals(self):
if 'goals' in self.file_source:
return self.file_source['goals']
return None