815 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			815 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from .Utils import data_path, __version__ | ||
|  | from .Colors import * | ||
|  | import logging | ||
|  | import worlds.oot.Music as music | ||
|  | import worlds.oot.Sounds as sfx | ||
|  | import worlds.oot.IconManip as icon | ||
|  | from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict | ||
|  | import json | ||
|  | 
 | ||
|  | logger = logging.getLogger('') | ||
|  | 
 | ||
|  | # Options are all lowercase and have underscores instead of spaces | ||
|  | # this needs to be undone for the oot generator | ||
|  | def format_cosmetic_option_result(option_result): | ||
|  |     def format_word(word): | ||
|  |         special_words = { | ||
|  |             'nes': 'NES', | ||
|  |             'gamecube': 'GameCube', | ||
|  |             'of': 'of' | ||
|  |         } | ||
|  |         return special_words.get(word, word.capitalize()) | ||
|  |     words = option_result.split('_') | ||
|  |     return ' '.join([format_word(word) for word in words]) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_targeting(rom, ootworld, symbols): | ||
|  |     # Set default targeting option to Hold | ||
|  |     if ootworld.default_targeting == 'hold': | ||
|  |         rom.write_byte(0xB71E6D, 0x01) | ||
|  |     else: | ||
|  |         rom.write_byte(0xB71E6D, 0x00) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_dpad(rom, ootworld, symbols): | ||
|  |     # Display D-Pad HUD | ||
|  |     if ootworld.display_dpad: | ||
|  |         rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x01) | ||
|  |     else: | ||
|  |         rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x00) | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_music(rom, ootworld, symbols): | ||
|  |     # patch music | ||
|  |     if ootworld.background_music != 'normal' or ootworld.fanfares != 'normal': | ||
|  |         music.restore_music(rom) | ||
|  |         log, errors = music.randomize_music(rom, ootworld, {}) | ||
|  |         if errors: | ||
|  |             logger.error(errors) | ||
|  |     else: | ||
|  |         music.restore_music(rom) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_model_colors(rom, color, model_addresses): | ||
|  |     main_addresses, dark_addresses = model_addresses | ||
|  | 
 | ||
|  |     if color is None: | ||
|  |         for address in main_addresses + dark_addresses: | ||
|  |             original = rom.original.read_bytes(address, 3) | ||
|  |             rom.write_bytes(address, original) | ||
|  |         return | ||
|  | 
 | ||
|  |     for address in main_addresses: | ||
|  |         rom.write_bytes(address, color) | ||
|  | 
 | ||
|  |     darkened_color = list(map(lambda light: int(max((light - 0x32) * 0.6, 0)), color)) | ||
|  |     for address in dark_addresses: | ||
|  |         rom.write_bytes(address, darkened_color) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_tunic_icon(rom, tunic, color): | ||
|  |     # patch tunic icon colors | ||
|  |     icon_locations = { | ||
|  |         'Kokiri Tunic': 0x007FE000, | ||
|  |         'Goron Tunic': 0x007FF000, | ||
|  |         'Zora Tunic': 0x00800000, | ||
|  |     } | ||
|  | 
 | ||
|  |     if color is not None: | ||
|  |         tunic_icon = icon.generate_tunic_icon(color) | ||
|  |     else: | ||
|  |         tunic_icon = rom.original.read_bytes(icon_locations[tunic], 0x1000) | ||
|  | 
 | ||
|  |     rom.write_bytes(icon_locations[tunic], tunic_icon) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_tunic_colors(rom, ootworld, symbols): | ||
|  |     # patch tunic colors | ||
|  |     tunics = [ | ||
|  |         ('Kokiri Tunic', 'kokiri_color', 0x00B6DA38), | ||
|  |         ('Goron Tunic',  'goron_color',  0x00B6DA3B), | ||
|  |         ('Zora Tunic',   'zora_color',   0x00B6DA3E), | ||
|  |     ] | ||
|  |     tunic_color_list = get_tunic_colors() | ||
|  | 
 | ||
|  |     for tunic, tunic_setting, address in tunics: | ||
|  |         tunic_option = format_cosmetic_option_result(ootworld.__dict__[tunic_setting]) | ||
|  | 
 | ||
|  |         # handle random | ||
|  |         if tunic_option == 'Random Choice': | ||
|  |             tunic_option = random.choice(tunic_color_list) | ||
|  |         # handle completely random | ||
|  |         if tunic_option == 'Completely Random': | ||
|  |             color = generate_random_color() | ||
|  |         # grab the color from the list | ||
|  |         elif tunic_option in tunic_colors: | ||
|  |             color = list(tunic_colors[tunic_option]) | ||
|  |         # build color from hex code | ||
|  |         else: | ||
|  |             color = hex_to_color(tunic_option) | ||
|  |             tunic_option = 'Custom' | ||
|  |         # "Weird" weirdshots will crash if the Kokiri Tunic Green value is > 0x99. Brickwall it. | ||
|  |         if ootworld.logic_rules != 'glitchless' and tunic == 'Kokiri Tunic': | ||
|  |             color[1] = min(color[1],0x98) | ||
|  |         rom.write_bytes(address, color) | ||
|  | 
 | ||
|  |         # patch the tunic icon | ||
|  |         if [tunic, tunic_option] not in [['Kokiri Tunic', 'Kokiri Green'], ['Goron Tunic', 'Goron Red'], ['Zora Tunic', 'Zora Blue']]: | ||
|  |             patch_tunic_icon(rom, tunic, color) | ||
|  |         else: | ||
|  |             patch_tunic_icon(rom, tunic, None) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_navi_colors(rom, ootworld, symbols): | ||
|  |     # patch navi colors | ||
|  |     navi = [ | ||
|  |         # colors for Navi | ||
|  |         ('Navi Idle',            'navi_color_default', | ||
|  |          [0x00B5E184],  # Default (Player) | ||
|  |          symbols.get('CFG_RAINBOW_NAVI_IDLE_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_IDLE_OUTER_ENABLED', None)), | ||
|  |         ('Navi Targeting Enemy', 'navi_color_enemy', | ||
|  |             [0x00B5E19C, 0x00B5E1BC],  # Enemy, Boss | ||
|  |             symbols.get('CFG_RAINBOW_NAVI_ENEMY_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_ENEMY_OUTER_ENABLED', None)), | ||
|  |         ('Navi Targeting NPC',   'navi_color_npc', | ||
|  |             [0x00B5E194], # NPC | ||
|  |             symbols.get('CFG_RAINBOW_NAVI_NPC_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_NPC_OUTER_ENABLED', None)), | ||
|  |         ('Navi Targeting Prop',  'navi_color_prop', | ||
|  |             [0x00B5E174, 0x00B5E17C, 0x00B5E18C, 0x00B5E1A4, 0x00B5E1AC, | ||
|  |              0x00B5E1B4, 0x00B5E1C4, 0x00B5E1CC, 0x00B5E1D4], # Everything else | ||
|  |             symbols.get('CFG_RAINBOW_NAVI_PROP_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_PROP_OUTER_ENABLED', None)), | ||
|  |     ] | ||
|  | 
 | ||
|  |     navi_color_list = get_navi_colors() | ||
|  |     rainbow_error = None | ||
|  | 
 | ||
|  |     for navi_action, navi_setting, navi_addresses, rainbow_inner_symbol, rainbow_outer_symbol in navi: | ||
|  |         navi_option_inner = format_cosmetic_option_result(ootworld.__dict__[navi_setting+'_inner']) | ||
|  |         navi_option_outer = format_cosmetic_option_result(ootworld.__dict__[navi_setting+'_outer']) | ||
|  | 
 | ||
|  |         # choose a random choice for the whole group | ||
|  |         if navi_option_inner == 'Random Choice': | ||
|  |             navi_option_inner = random.choice(navi_color_list) | ||
|  |         if navi_option_outer == 'Random Choice': | ||
|  |             navi_option_outer = random.choice(navi_color_list) | ||
|  | 
 | ||
|  |         if navi_option_outer == 'Match Inner': | ||
|  |             navi_option_outer = navi_option_inner | ||
|  | 
 | ||
|  |         colors = [] | ||
|  |         option_dict = {} | ||
|  |         for address_index, address in enumerate(navi_addresses): | ||
|  |             address_colors = {} | ||
|  |             colors.append(address_colors) | ||
|  |             for index, (navi_part, option, rainbow_symbol) in enumerate([ | ||
|  |                 ('inner', navi_option_inner, rainbow_inner_symbol), | ||
|  |                 ('outer', navi_option_outer, rainbow_outer_symbol), | ||
|  |             ]): | ||
|  |                 color = None | ||
|  | 
 | ||
|  |                 # set rainbow option | ||
|  |                 if rainbow_symbol is not None and option == 'Rainbow': | ||
|  |                     rom.write_byte(rainbow_symbol, 0x01) | ||
|  |                     color = [0x00, 0x00, 0x00] | ||
|  |                 elif rainbow_symbol is not None: | ||
|  |                     rom.write_byte(rainbow_symbol, 0x00) | ||
|  |                 elif option == 'Rainbow': | ||
|  |                     rainbow_error = "Rainbow Navi is not supported by this patch version. Using 'Completely Random' as a substitute." | ||
|  |                     option = 'Completely Random' | ||
|  | 
 | ||
|  |                 # completely random is random for every subgroup | ||
|  |                 if color is None and option == 'Completely Random': | ||
|  |                     color = generate_random_color() | ||
|  | 
 | ||
|  |                 # grab the color from the list | ||
|  |                 if color is None and option in NaviColors: | ||
|  |                     color = list(NaviColors[option][index]) | ||
|  | 
 | ||
|  |                 # build color from hex code | ||
|  |                 if color is None: | ||
|  |                     color = hex_to_color(option) | ||
|  |                     option = 'Custom' | ||
|  | 
 | ||
|  |                 # Check color validity | ||
|  |                 if color is None: | ||
|  |                     raise Exception(f'Invalid {navi_part} color {option} for {navi_action}') | ||
|  | 
 | ||
|  |                 address_colors[navi_part] = color | ||
|  |                 option_dict[navi_part] = option | ||
|  | 
 | ||
|  |             # write color | ||
|  |             color = address_colors['inner'] + [0xFF] + address_colors['outer'] + [0xFF] | ||
|  |             rom.write_bytes(address, color) | ||
|  | 
 | ||
|  | 
 | ||
|  |     if rainbow_error: | ||
|  |         logger.error(rainbow_error) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_sword_trails(rom, ootworld, symbols): | ||
|  |     # patch sword trail duration | ||
|  |     rom.write_byte(0x00BEFF8C, ootworld.sword_trail_duration) | ||
|  | 
 | ||
|  |     # patch sword trail colors | ||
|  |     sword_trails = [ | ||
|  |         ('Sword Trail', 'sword_trail_color', | ||
|  |             [(0x00BEFF7C, 0xB0, 0x40, 0xB0, 0xFF), (0x00BEFF84, 0x20, 0x00, 0x10, 0x00)], | ||
|  |             symbols.get('CFG_RAINBOW_SWORD_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_SWORD_OUTER_ENABLED', None)), | ||
|  |     ] | ||
|  | 
 | ||
|  |     sword_trail_color_list = get_sword_trail_colors() | ||
|  |     rainbow_error = None | ||
|  | 
 | ||
|  |     for trail_name, trail_setting, trail_addresses, rainbow_inner_symbol, rainbow_outer_symbol in sword_trails: | ||
|  |         option_inner = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_inner']) | ||
|  |         option_outer = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_outer']) | ||
|  | 
 | ||
|  |         # handle random choice | ||
|  |         if option_inner == 'Random Choice': | ||
|  |             option_inner = random.choice(sword_trail_color_list) | ||
|  |         if option_outer == 'Random Choice': | ||
|  |             option_outer = random.choice(sword_trail_color_list) | ||
|  | 
 | ||
|  |         if option_outer == 'Match Inner': | ||
|  |             option_outer = option_inner | ||
|  | 
 | ||
|  |         colors = [] | ||
|  |         option_dict = {} | ||
|  |         for address_index, (address, inner_transparency, inner_white_transparency, outer_transparency, outer_white_transparency) in enumerate(trail_addresses): | ||
|  |             address_colors = {} | ||
|  |             colors.append(address_colors) | ||
|  |             transparency_dict = {} | ||
|  |             for index, (trail_part, option, rainbow_symbol, white_transparency, transparency) in enumerate([ | ||
|  |                 ('inner', option_inner, rainbow_inner_symbol, inner_white_transparency, inner_transparency), | ||
|  |                 ('outer', option_outer, rainbow_outer_symbol, outer_white_transparency, outer_transparency), | ||
|  |             ]): | ||
|  |                 color = None | ||
|  | 
 | ||
|  |                 # set rainbow option | ||
|  |                 if rainbow_symbol is not None and option == 'Rainbow': | ||
|  |                     rom.write_byte(rainbow_symbol, 0x01) | ||
|  |                     color = [0x00, 0x00, 0x00] | ||
|  |                 elif rainbow_symbol is not None: | ||
|  |                     rom.write_byte(rainbow_symbol, 0x00) | ||
|  |                 elif option == 'Rainbow': | ||
|  |                     rainbow_error = "Rainbow Sword Trail is not supported by this patch version. Using 'Completely Random' as a substitute." | ||
|  |                     option = 'Completely Random' | ||
|  | 
 | ||
|  |                 # completely random is random for every subgroup | ||
|  |                 if color is None and option == 'Completely Random': | ||
|  |                     color = generate_random_color() | ||
|  | 
 | ||
|  |                 # grab the color from the list | ||
|  |                 if color is None and option in sword_trail_colors: | ||
|  |                     color = list(sword_trail_colors[option]) | ||
|  | 
 | ||
|  |                 # build color from hex code | ||
|  |                 if color is None: | ||
|  |                     color = hex_to_color(option) | ||
|  |                     option = 'Custom' | ||
|  | 
 | ||
|  |                 # Check color validity | ||
|  |                 if color is None: | ||
|  |                     raise Exception(f'Invalid {trail_part} color {option} for {trail_name}') | ||
|  | 
 | ||
|  |                 # handle white transparency | ||
|  |                 if option == 'White': | ||
|  |                     transparency_dict[trail_part] = white_transparency | ||
|  |                 else: | ||
|  |                     transparency_dict[trail_part] = transparency | ||
|  | 
 | ||
|  |                 address_colors[trail_part] = color | ||
|  |                 option_dict[trail_part] = option | ||
|  | 
 | ||
|  |             # write color | ||
|  |             color = address_colors['outer'] + [transparency_dict['outer']] + address_colors['inner'] + [transparency_dict['inner']] | ||
|  |             rom.write_bytes(address, color) | ||
|  | 
 | ||
|  |     if rainbow_error: | ||
|  |         logger.error(rainbow_error) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_bombchu_trails(rom, ootworld, symbols): | ||
|  |     # patch bombchu trail colors | ||
|  |     bombchu_trails = [ | ||
|  |         ('Bombchu Trail', 'bombchu_trail_color', get_bombchu_trail_colors(), bombchu_trail_colors, | ||
|  |             (symbols['CFG_BOMBCHU_TRAIL_INNER_COLOR'], symbols['CFG_BOMBCHU_TRAIL_OUTER_COLOR'], | ||
|  |              symbols['CFG_RAINBOW_BOMBCHU_TRAIL_INNER_ENABLED'], symbols['CFG_RAINBOW_BOMBCHU_TRAIL_OUTER_ENABLED'])), | ||
|  |     ] | ||
|  | 
 | ||
|  |     patch_trails(rom, ootworld, bombchu_trails) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_boomerang_trails(rom, ootworld, symbols): | ||
|  |     # patch boomerang trail colors | ||
|  |     boomerang_trails = [ | ||
|  |         ('Boomerang Trail', 'boomerang_trail_color', get_boomerang_trail_colors(), boomerang_trail_colors, | ||
|  |             (symbols['CFG_BOOM_TRAIL_INNER_COLOR'], symbols['CFG_BOOM_TRAIL_OUTER_COLOR'], | ||
|  |              symbols['CFG_RAINBOW_BOOM_TRAIL_INNER_ENABLED'], symbols['CFG_RAINBOW_BOOM_TRAIL_OUTER_ENABLED'])), | ||
|  |     ] | ||
|  | 
 | ||
|  |     patch_trails(rom, ootworld, boomerang_trails) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_trails(rom, ootworld, trails): | ||
|  |     for trail_name, trail_setting, trail_color_list, trail_color_dict, trail_symbols in trails: | ||
|  |         color_inner_symbol, color_outer_symbol, rainbow_inner_symbol, rainbow_outer_symbol = trail_symbols | ||
|  |         option_inner = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_inner']) | ||
|  |         option_outer = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_outer']) | ||
|  | 
 | ||
|  |         # handle random choice | ||
|  |         if option_inner == 'Random Choice': | ||
|  |             option_inner = random.choice(trail_color_list) | ||
|  |         if option_outer == 'Random Choice': | ||
|  |             option_outer = random.choice(trail_color_list) | ||
|  | 
 | ||
|  |         if option_outer == 'Match Inner': | ||
|  |             option_outer = option_inner | ||
|  | 
 | ||
|  |         option_dict = {} | ||
|  |         colors = {} | ||
|  | 
 | ||
|  |         for index, (trail_part, option, rainbow_symbol, color_symbol) in enumerate([ | ||
|  |             ('inner', option_inner, rainbow_inner_symbol, color_inner_symbol), | ||
|  |             ('outer', option_outer, rainbow_outer_symbol, color_outer_symbol), | ||
|  |         ]): | ||
|  |             color = None | ||
|  | 
 | ||
|  |             # set rainbow option | ||
|  |             if option == 'Rainbow': | ||
|  |                 rom.write_byte(rainbow_symbol, 0x01) | ||
|  |                 color = [0x00, 0x00, 0x00] | ||
|  |             else: | ||
|  |                 rom.write_byte(rainbow_symbol, 0x00) | ||
|  | 
 | ||
|  |             # handle completely random | ||
|  |             if color is None and option == 'Completely Random': | ||
|  |                 # Specific handling for inner bombchu trails for contrast purposes. | ||
|  |                 if trail_name == 'Bombchu Trail' and trail_part == 'inner': | ||
|  |                     fixed_dark_color = [0, 0, 0] | ||
|  |                     color = [0, 0, 0] | ||
|  |                     # Avoid colors which have a low contrast so the bombchu ticking is still visible | ||
|  |                     while contrast_ratio(color, fixed_dark_color) <= 4: | ||
|  |                         color = generate_random_color() | ||
|  |                 else: | ||
|  |                     color = generate_random_color() | ||
|  | 
 | ||
|  |             # grab the color from the list | ||
|  |             if color is None and option in trail_color_dict: | ||
|  |                 color = list(trail_color_dict[option]) | ||
|  | 
 | ||
|  |             # build color from hex code | ||
|  |             if color is None: | ||
|  |                 color = hex_to_color(option) | ||
|  |                 option = 'Custom' | ||
|  | 
 | ||
|  |             option_dict[trail_part] = option | ||
|  |             colors[trail_part] = color | ||
|  | 
 | ||
|  |             # write color | ||
|  |             rom.write_bytes(color_symbol, color) | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_gauntlet_colors(rom, ootworld, symbols): | ||
|  |     # patch gauntlet colors | ||
|  |     gauntlets = [ | ||
|  |         ('Silver Gauntlets', 'silver_gauntlets_color', 0x00B6DA44, | ||
|  |             ([0x173B4CC], [0x173B4D4, 0x173B50C, 0x173B514])), # GI Model DList colors | ||
|  |         ('Gold Gauntlets', 'golden_gauntlets_color',  0x00B6DA47, | ||
|  |             ([0x173B4EC], [0x173B4F4, 0x173B52C, 0x173B534])), # GI Model DList colors | ||
|  |     ] | ||
|  |     gauntlet_color_list = get_gauntlet_colors() | ||
|  | 
 | ||
|  |     for gauntlet, gauntlet_setting, address, model_addresses in gauntlets: | ||
|  |         gauntlet_option = format_cosmetic_option_result(ootworld.__dict__[gauntlet_setting]) | ||
|  | 
 | ||
|  |         # handle random | ||
|  |         if gauntlet_option == 'Random Choice': | ||
|  |             gauntlet_option = random.choice(gauntlet_color_list) | ||
|  |         # handle completely random | ||
|  |         if gauntlet_option == 'Completely Random': | ||
|  |             color = generate_random_color() | ||
|  |         # grab the color from the list | ||
|  |         elif gauntlet_option in gauntlet_colors: | ||
|  |             color = list(gauntlet_colors[gauntlet_option]) | ||
|  |         # build color from hex code | ||
|  |         else: | ||
|  |             color = hex_to_color(gauntlet_option) | ||
|  |             gauntlet_option = 'Custom' | ||
|  |         rom.write_bytes(address, color) | ||
|  |         if ootworld.correct_model_colors: | ||
|  |             patch_model_colors(rom, color, model_addresses) | ||
|  |         else: | ||
|  |             patch_model_colors(rom, None, model_addresses) | ||
|  | 
 | ||
|  | def patch_shield_frame_colors(rom, ootworld, symbols): | ||
|  |     # patch shield frame colors | ||
|  |     shield_frames = [ | ||
|  |         ('Mirror Shield Frame', 'mirror_shield_frame_color', | ||
|  |             [0xFA7274, 0xFA776C, 0xFAA27C, 0xFAC564, 0xFAC984, 0xFAEDD4], | ||
|  |             ([0x1616FCC], [0x1616FD4])), | ||
|  |     ] | ||
|  |     shield_frame_color_list = get_shield_frame_colors() | ||
|  | 
 | ||
|  |     for shield_frame, shield_frame_setting, addresses, model_addresses in shield_frames: | ||
|  |         shield_frame_option = format_cosmetic_option_result(ootworld.__dict__[shield_frame_setting]) | ||
|  | 
 | ||
|  |         # handle random | ||
|  |         if shield_frame_option == 'Random Choice': | ||
|  |             shield_frame_option = random.choice(shield_frame_color_list) | ||
|  |         # handle completely random | ||
|  |         if shield_frame_option == 'Completely Random': | ||
|  |             color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)] | ||
|  |         # grab the color from the list | ||
|  |         elif shield_frame_option in shield_frame_colors: | ||
|  |             color = list(shield_frame_colors[shield_frame_option]) | ||
|  |         # build color from hex code | ||
|  |         else: | ||
|  |             color = hex_to_color(shield_frame_option) | ||
|  |             shield_frame_option = 'Custom' | ||
|  |         for address in addresses: | ||
|  |             rom.write_bytes(address, color) | ||
|  |         if ootworld.correct_model_colors and shield_frame_option != 'Red': | ||
|  |             patch_model_colors(rom, color, model_addresses) | ||
|  |         else: | ||
|  |             patch_model_colors(rom, None, model_addresses) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_heart_colors(rom, ootworld, symbols): | ||
|  |     # patch heart colors | ||
|  |     hearts = [ | ||
|  |         ('Heart Color', 'heart_color', symbols['CFG_HEART_COLOR'], 0xBB0994, | ||
|  |             ([0x14DA474, 0x14DA594, 0x14B701C, 0x14B70DC], | ||
|  |              [0x14B70FC, 0x14DA494, 0x14DA5B4, 0x14B700C, 0x14B702C, 0x14B703C, 0x14B704C, 0x14B705C, | ||
|  |               0x14B706C, 0x14B707C, 0x14B708C, 0x14B709C, 0x14B70AC, 0x14B70BC, 0x14B70CC])), # GI Model DList colors | ||
|  |     ] | ||
|  |     heart_color_list = get_heart_colors() | ||
|  | 
 | ||
|  |     for heart, heart_setting, symbol, file_select_address, model_addresses in hearts: | ||
|  |         heart_option = format_cosmetic_option_result(ootworld.__dict__[heart_setting]) | ||
|  | 
 | ||
|  |         # handle random | ||
|  |         if heart_option == 'Random Choice': | ||
|  |             heart_option = random.choice(heart_color_list) | ||
|  |         # handle completely random | ||
|  |         if heart_option == 'Completely Random': | ||
|  |             color = generate_random_color() | ||
|  |         # grab the color from the list | ||
|  |         elif heart_option in heart_colors: | ||
|  |             color = list(heart_colors[heart_option]) | ||
|  |         # build color from hex code | ||
|  |         else: | ||
|  |             color = hex_to_color(heart_option) | ||
|  |             heart_option = 'Custom' | ||
|  |         rom.write_int16s(symbol, color) # symbol for ingame HUD | ||
|  |         rom.write_int16s(file_select_address, color) # file select normal hearts | ||
|  |         if heart_option != 'Red': | ||
|  |             rom.write_int16s(file_select_address + 6, color) # file select DD hearts | ||
|  |         else: | ||
|  |             original_dd_color = rom.original.read_bytes(file_select_address + 6, 6) | ||
|  |             rom.write_bytes(file_select_address + 6, original_dd_color) | ||
|  |         if ootworld.correct_model_colors and heart_option != 'Red': | ||
|  |             patch_model_colors(rom, color, model_addresses) # heart model colors | ||
|  |             icon.patch_overworld_icon(rom, color, 0xF43D80) # Overworld Heart Icon | ||
|  |         else: | ||
|  |             patch_model_colors(rom, None, model_addresses) | ||
|  |             icon.patch_overworld_icon(rom, None, 0xF43D80) | ||
|  | 
 | ||
|  | def patch_magic_colors(rom, ootworld, symbols): | ||
|  |     # patch magic colors | ||
|  |     magic = [ | ||
|  |         ('Magic Meter Color', 'magic_color', symbols["CFG_MAGIC_COLOR"], | ||
|  |             ([0x154C654, 0x154CFB4], [0x154C65C, 0x154CFBC])), # GI Model DList colors | ||
|  |     ] | ||
|  |     magic_color_list = get_magic_colors() | ||
|  | 
 | ||
|  |     for magic_color, magic_setting, symbol, model_addresses in magic: | ||
|  |         magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting]) | ||
|  | 
 | ||
|  |         if magic_option == 'Random Choice': | ||
|  |            magic_option = random.choice(magic_color_list) | ||
|  | 
 | ||
|  |         if magic_option == 'Completely Random': | ||
|  |             color = generate_random_color() | ||
|  |         elif magic_option in magic_colors: | ||
|  |             color = list(magic_colors[magic_option]) | ||
|  |         else: | ||
|  |             color = hex_to_color(magic_option) | ||
|  |             magic_option = 'Custom' | ||
|  |         rom.write_int16s(symbol, color) | ||
|  |         if magic_option != 'Green' and ootworld.correct_model_colors: | ||
|  |             patch_model_colors(rom, color, model_addresses) | ||
|  |             icon.patch_overworld_icon(rom, color, 0xF45650, data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot | ||
|  |             icon.patch_overworld_icon(rom, color, 0xF47650, data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot | ||
|  |         else: | ||
|  |             patch_model_colors(rom, None, model_addresses) | ||
|  |             icon.patch_overworld_icon(rom, None, 0xF45650) | ||
|  |             icon.patch_overworld_icon(rom, None, 0xF47650) | ||
|  | 
 | ||
|  | def patch_button_colors(rom, ootworld, symbols): | ||
|  |     buttons = [ | ||
|  |         ('A Button Color', 'a_button_color', a_button_colors, | ||
|  |             [('A Button Color', symbols['CFG_A_BUTTON_COLOR'], | ||
|  |                 None), | ||
|  |              ('Text Cursor Color', symbols['CFG_TEXT_CURSOR_COLOR'], | ||
|  |                 [(0xB88E81, 0xB88E85, 0xB88E9)]), # Initial Inner Color | ||
|  |              ('Shop Cursor Color', symbols['CFG_SHOP_CURSOR_COLOR'], | ||
|  |                 None), | ||
|  |              ('Save/Death Cursor Color', None, | ||
|  |                 [(0xBBEBC2, 0xBBEBC3, 0xBBEBD6), (0xBBEDDA, 0xBBEDDB, 0xBBEDDE)]), # Save Cursor / Death Cursor | ||
|  |              ('Pause Menu A Cursor Color', None, | ||
|  |                 [(0xBC7849, 0xBC784B, 0xBC784D), (0xBC78A9, 0xBC78AB, 0xBC78AD), (0xBC78BB, 0xBC78BD, 0xBC78BF)]), # Inner / Pulse 1 / Pulse 2 | ||
|  |              ('Pause Menu A Icon Color', None, | ||
|  |                 [(0x845754, 0x845755, 0x845756)]), | ||
|  |              ('A Note Color', symbols['CFG_A_NOTE_COLOR'], # For Textbox Song Display | ||
|  |                 [(0xBB299A, 0xBB299B, 0xBB299E), (0xBB2C8E, 0xBB2C8F, 0xBB2C92), (0xBB2F8A, 0xBB2F8B, 0xBB2F96)]), # Pause Menu Song Display | ||
|  |             ]), | ||
|  |         ('B Button Color', 'b_button_color', b_button_colors, | ||
|  |             [('B Button Color', symbols['CFG_B_BUTTON_COLOR'], | ||
|  |                 None), | ||
|  |             ]), | ||
|  |         ('C Button Color', 'c_button_color', c_button_colors, | ||
|  |             [('C Button Color', symbols['CFG_C_BUTTON_COLOR'], | ||
|  |                 None), | ||
|  |              ('Pause Menu C Cursor Color', None, | ||
|  |                 [(0xBC7843, 0xBC7845, 0xBC7847), (0xBC7891, 0xBC7893, 0xBC7895), (0xBC78A3, 0xBC78A5, 0xBC78A7)]), # Inner / Pulse 1 / Pulse 2 | ||
|  |              ('Pause Menu C Icon Color', None, | ||
|  |                 [(0x8456FC, 0x8456FD, 0x8456FE)]), | ||
|  |              ('C Note Color', symbols['CFG_C_NOTE_COLOR'], # For Textbox Song Display | ||
|  |                 [(0xBB2996, 0xBB2997, 0xBB29A2), (0xBB2C8A, 0xBB2C8B, 0xBB2C96), (0xBB2F86, 0xBB2F87, 0xBB2F9A)]), # Pause Menu Song Display | ||
|  |             ]), | ||
|  |         ('Start Button Color', 'start_button_color', start_button_colors, | ||
|  |             [('Start Button Color', None, | ||
|  |                 [(0xAE9EC6, 0xAE9EC7, 0xAE9EDA)]), | ||
|  |             ]), | ||
|  |     ] | ||
|  | 
 | ||
|  |     for button, button_setting, button_colors, patches in buttons: | ||
|  |         button_option = format_cosmetic_option_result(ootworld.__dict__[button_setting]) | ||
|  |         color_set = None | ||
|  |         colors = {} | ||
|  | 
 | ||
|  |         # handle random | ||
|  |         if button_option == 'Random Choice': | ||
|  |             button_option = random.choice(list(button_colors.keys())) | ||
|  |         # handle completely random | ||
|  |         if button_option == 'Completely Random': | ||
|  |             fixed_font_color = [10, 10, 10] | ||
|  |             color = [0, 0, 0] | ||
|  |             # Avoid colors which have a low contrast with the font inside buttons (eg. the A letter) | ||
|  |             while contrast_ratio(color, fixed_font_color) <= 3: | ||
|  |                 color = generate_random_color() | ||
|  |         # grab the color from the list | ||
|  |         elif button_option in button_colors: | ||
|  |             color_set = [button_colors[button_option]] if isinstance(button_colors[button_option][0], int) else list(button_colors[button_option]) | ||
|  |             color = color_set[0] | ||
|  |         # build color from hex code | ||
|  |         else: | ||
|  |             color = hex_to_color(button_option) | ||
|  |             button_option = 'Custom' | ||
|  | 
 | ||
|  |         # apply all button color patches | ||
|  |         for i, (patch, symbol, byte_addresses) in enumerate(patches): | ||
|  |             if color_set is not None and len(color_set) > i and color_set[i]: | ||
|  |                 colors[patch] = color_set[i] | ||
|  |             else: | ||
|  |                 colors[patch] = color | ||
|  | 
 | ||
|  |             if symbol: | ||
|  |                 rom.write_int16s(symbol, colors[patch]) | ||
|  | 
 | ||
|  |             if byte_addresses: | ||
|  |                 for r_addr, g_addr, b_addr in byte_addresses: | ||
|  |                     rom.write_byte(r_addr, colors[patch][0]) | ||
|  |                     rom.write_byte(g_addr, colors[patch][1]) | ||
|  |                     rom.write_byte(b_addr, colors[patch][2]) | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_sfx(rom, ootworld, symbols): | ||
|  |     # Configurable Sound Effects | ||
|  |     sfx_config = [ | ||
|  |           ('sfx_navi_overworld', sfx.SoundHooks.NAVI_OVERWORLD), | ||
|  |           ('sfx_navi_enemy',     sfx.SoundHooks.NAVI_ENEMY), | ||
|  |           ('sfx_low_hp',         sfx.SoundHooks.HP_LOW), | ||
|  |           ('sfx_menu_cursor',    sfx.SoundHooks.MENU_CURSOR), | ||
|  |           ('sfx_menu_select',    sfx.SoundHooks.MENU_SELECT), | ||
|  |           ('sfx_nightfall',      sfx.SoundHooks.NIGHTFALL), | ||
|  |           ('sfx_horse_neigh',    sfx.SoundHooks.HORSE_NEIGH), | ||
|  |           ('sfx_hover_boots',    sfx.SoundHooks.BOOTS_HOVER), | ||
|  |     ] | ||
|  |     sound_dict = sfx.get_patch_dict() | ||
|  |     sounds_keyword_label = {sound.value.keyword: sound.value.label for sound in sfx.Sounds} | ||
|  |     sounds_label_keyword = {sound.value.label: sound.value.keyword for sound in sfx.Sounds} | ||
|  | 
 | ||
|  |     for setting, hook in sfx_config: | ||
|  |         selection = ootworld.__dict__[setting].replace('_', '-') | ||
|  | 
 | ||
|  |         if selection == 'default': | ||
|  |             for loc in hook.value.locations: | ||
|  |                 sound_id = rom.original.read_int16(loc) | ||
|  |                 rom.write_int16(loc, sound_id) | ||
|  |         else: | ||
|  |             if selection == 'random-choice': | ||
|  |                 selection = random.choice(sfx.get_hook_pool(hook)).value.keyword | ||
|  |             elif selection == 'random-ear-safe': | ||
|  |                 selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword | ||
|  |             elif selection == 'completely-random': | ||
|  |                 selection = random.choice(sfx.standard).value.keyword | ||
|  |             sound_id  = sound_dict[selection] | ||
|  |             for loc in hook.value.locations: | ||
|  |                 rom.write_int16(loc, sound_id) | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_instrument(rom, ootworld, symbols): | ||
|  |     # Player Instrument | ||
|  |     instruments = { | ||
|  |            #'none':            0x00, | ||
|  |             'ocarina':         0x01, | ||
|  |             'malon':           0x02, | ||
|  |             'whistle':         0x03, | ||
|  |             'harp':            0x04, | ||
|  |             'grind_organ':     0x05, | ||
|  |             'flute':           0x06, | ||
|  |            #'another_ocarina': 0x07, | ||
|  |     } | ||
|  | 
 | ||
|  |     choice = ootworld.sfx_ocarina | ||
|  |     if choice == 'random-choice': | ||
|  |         choice = random.choice(list(instruments.keys())) | ||
|  | 
 | ||
|  |     rom.write_byte(0x00B53C7B, instruments[choice]) | ||
|  |     rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods | ||
|  | 
 | ||
|  | 
 | ||
|  | legacy_cosmetic_data_headers = [ | ||
|  |     0x03481000, | ||
|  |     0x03480810, | ||
|  | ] | ||
|  | 
 | ||
|  | global_patch_sets = [ | ||
|  |     patch_targeting, | ||
|  |     patch_music, | ||
|  |     patch_tunic_colors, | ||
|  |     patch_navi_colors, | ||
|  |     patch_sword_trails, | ||
|  |     patch_gauntlet_colors, | ||
|  |     patch_shield_frame_colors, | ||
|  |     patch_sfx, | ||
|  |     patch_instrument, | ||
|  | ] | ||
|  | 
 | ||
|  | patch_sets = { | ||
|  |     0x1F04FA62: { | ||
|  |         "patches": [ | ||
|  |             patch_dpad, | ||
|  |             patch_sword_trails, | ||
|  |         ], | ||
|  |         "symbols": { | ||
|  |             "CFG_DISPLAY_DPAD": 0x0004, | ||
|  |             "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0005, | ||
|  |             "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0006, | ||
|  |         }, | ||
|  |     }, | ||
|  |     0x1F05D3F9: { | ||
|  |         "patches": [ | ||
|  |             patch_dpad, | ||
|  |             patch_sword_trails, | ||
|  |         ], | ||
|  |         "symbols": { | ||
|  |             "CFG_DISPLAY_DPAD": 0x0004, | ||
|  |             "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0005, | ||
|  |             "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0006, | ||
|  |         }, | ||
|  |     }, | ||
|  |     0x1F0693FB: { | ||
|  |         "patches": [ | ||
|  |             patch_dpad, | ||
|  |             patch_sword_trails, | ||
|  |             patch_heart_colors, | ||
|  |             patch_magic_colors, | ||
|  |         ], | ||
|  |         "symbols": { | ||
|  |             "CFG_MAGIC_COLOR": 0x0004, | ||
|  |             "CFG_HEART_COLOR": 0x000A, | ||
|  |             "CFG_DISPLAY_DPAD": 0x0010, | ||
|  |             "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0011, | ||
|  |             "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0012, | ||
|  |         } | ||
|  |     }, | ||
|  |     0x1F073FC9: { | ||
|  |         "patches": [ | ||
|  |             patch_dpad, | ||
|  |             patch_sword_trails, | ||
|  |             patch_heart_colors, | ||
|  |             patch_magic_colors, | ||
|  |             patch_button_colors, | ||
|  |         ], | ||
|  |         "symbols": { | ||
|  |             "CFG_MAGIC_COLOR": 0x0004, | ||
|  |             "CFG_HEART_COLOR": 0x000A, | ||
|  |             "CFG_A_BUTTON_COLOR": 0x0010, | ||
|  |             "CFG_B_BUTTON_COLOR": 0x0016, | ||
|  |             "CFG_C_BUTTON_COLOR": 0x001C, | ||
|  |             "CFG_TEXT_CURSOR_COLOR": 0x0022, | ||
|  |             "CFG_SHOP_CURSOR_COLOR": 0x0028, | ||
|  |             "CFG_A_NOTE_COLOR": 0x002E, | ||
|  |             "CFG_C_NOTE_COLOR": 0x0034, | ||
|  |             "CFG_DISPLAY_DPAD": 0x003A, | ||
|  |             "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x003B, | ||
|  |             "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x003C, | ||
|  |         } | ||
|  |     }, | ||
|  |     0x1F073FD8: { | ||
|  |         "patches": [ | ||
|  |             patch_dpad, | ||
|  |             patch_navi_colors, | ||
|  |             patch_sword_trails, | ||
|  |             patch_heart_colors, | ||
|  |             patch_magic_colors, | ||
|  |             patch_button_colors, | ||
|  |             patch_boomerang_trails, | ||
|  |             patch_bombchu_trails, | ||
|  |         ], | ||
|  |         "symbols": { | ||
|  |             "CFG_MAGIC_COLOR": 0x0004, | ||
|  |             "CFG_HEART_COLOR": 0x000A, | ||
|  |             "CFG_A_BUTTON_COLOR": 0x0010, | ||
|  |             "CFG_B_BUTTON_COLOR": 0x0016, | ||
|  |             "CFG_C_BUTTON_COLOR": 0x001C, | ||
|  |             "CFG_TEXT_CURSOR_COLOR": 0x0022, | ||
|  |             "CFG_SHOP_CURSOR_COLOR": 0x0028, | ||
|  |             "CFG_A_NOTE_COLOR": 0x002E, | ||
|  |             "CFG_C_NOTE_COLOR": 0x0034, | ||
|  |             "CFG_BOOM_TRAIL_INNER_COLOR": 0x003A, | ||
|  |             "CFG_BOOM_TRAIL_OUTER_COLOR": 0x003D, | ||
|  |             "CFG_BOMBCHU_TRAIL_INNER_COLOR": 0x0040, | ||
|  |             "CFG_BOMBCHU_TRAIL_OUTER_COLOR": 0x0043, | ||
|  |             "CFG_DISPLAY_DPAD": 0x0046, | ||
|  |             "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0047, | ||
|  |             "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0048, | ||
|  |             "CFG_RAINBOW_BOOM_TRAIL_INNER_ENABLED": 0x0049, | ||
|  |             "CFG_RAINBOW_BOOM_TRAIL_OUTER_ENABLED": 0x004A, | ||
|  |             "CFG_RAINBOW_BOMBCHU_TRAIL_INNER_ENABLED": 0x004B, | ||
|  |             "CFG_RAINBOW_BOMBCHU_TRAIL_OUTER_ENABLED": 0x004C, | ||
|  |             "CFG_RAINBOW_NAVI_IDLE_INNER_ENABLED": 0x004D, | ||
|  |             "CFG_RAINBOW_NAVI_IDLE_OUTER_ENABLED": 0x004E, | ||
|  |             "CFG_RAINBOW_NAVI_ENEMY_INNER_ENABLED": 0x004F, | ||
|  |             "CFG_RAINBOW_NAVI_ENEMY_OUTER_ENABLED": 0x0050, | ||
|  |             "CFG_RAINBOW_NAVI_NPC_INNER_ENABLED": 0x0051, | ||
|  |             "CFG_RAINBOW_NAVI_NPC_OUTER_ENABLED": 0x0052, | ||
|  |             "CFG_RAINBOW_NAVI_PROP_INNER_ENABLED": 0x0053, | ||
|  |             "CFG_RAINBOW_NAVI_PROP_OUTER_ENABLED": 0x0054, | ||
|  |         } | ||
|  |     }, | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | def patch_cosmetics(ootworld, rom): | ||
|  |     # Use the world's slot seed for cosmetics | ||
|  |     random.seed(ootworld.world.slot_seeds[ootworld.player]) | ||
|  | 
 | ||
|  |     # try to detect the cosmetic patch data format | ||
|  |     versioned_patch_set = None | ||
|  |     cosmetic_context = rom.read_int32(rom.sym('RANDO_CONTEXT') + 4) | ||
|  |     if cosmetic_context >= 0x80000000 and cosmetic_context <= 0x80F7FFFC: | ||
|  |         cosmetic_context = (cosmetic_context - 0x80400000) + 0x3480000 # convert from RAM to ROM address | ||
|  |         cosmetic_version = rom.read_int32(cosmetic_context) | ||
|  |         versioned_patch_set = patch_sets.get(cosmetic_version) | ||
|  |     else: | ||
|  |         # If cosmetic_context is not a valid pointer, then try to | ||
|  |         # search over all possible legacy header locations. | ||
|  |         for header in legacy_cosmetic_data_headers: | ||
|  |             cosmetic_context = header | ||
|  |             cosmetic_version = rom.read_int32(cosmetic_context) | ||
|  |             if cosmetic_version in patch_sets: | ||
|  |                 versioned_patch_set = patch_sets[cosmetic_version] | ||
|  |                 break | ||
|  | 
 | ||
|  |     # patch version specific patches | ||
|  |     if versioned_patch_set: | ||
|  |         # offset the cosmetic_context struct for absolute addressing | ||
|  |         cosmetic_context_symbols = { | ||
|  |             sym: address + cosmetic_context | ||
|  |             for sym, address in versioned_patch_set['symbols'].items() | ||
|  |         } | ||
|  | 
 | ||
|  |         # warn if patching a legacy format | ||
|  |         if cosmetic_version != rom.read_int32(rom.sym('COSMETIC_FORMAT_VERSION')): | ||
|  |             logger.error("ROM uses old cosmetic patch format.") | ||
|  | 
 | ||
|  |         # patch cosmetics that use vanilla oot data, and always compatible | ||
|  |         for patch_func in [patch for patch in global_patch_sets if patch not in versioned_patch_set['patches']]: | ||
|  |             patch_func(rom, ootworld, {}) | ||
|  | 
 | ||
|  |         for patch_func in versioned_patch_set['patches']: | ||
|  |             patch_func(rom, ootworld, cosmetic_context_symbols) | ||
|  |     else: | ||
|  |         # patch cosmetics that use vanilla oot data, and always compatible | ||
|  |         for patch_func in global_patch_sets: | ||
|  |             patch_func(rom, ootworld, {}) | ||
|  | 
 | ||
|  |         # Unknown patch format | ||
|  |         logger.error("Unable to patch some cosmetics. ROM uses unknown cosmetic patch format.") |