485 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			485 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import random
							 | 
						||
| 
								 | 
							
								import os
							 | 
						||
| 
								 | 
							
								from .Utils import compare_version, data_path
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Format: (Title, Sequence ID)
							 | 
						||
| 
								 | 
							
								bgm_sequence_ids = [
							 | 
						||
| 
								 | 
							
								    ("Hyrule Field", 0x02),
							 | 
						||
| 
								 | 
							
								    ("Dodongos Cavern", 0x18),
							 | 
						||
| 
								 | 
							
								    ("Kakariko Adult", 0x19),
							 | 
						||
| 
								 | 
							
								    ("Battle", 0x1A),
							 | 
						||
| 
								 | 
							
								    ("Boss Battle", 0x1B),
							 | 
						||
| 
								 | 
							
								    ("Inside Deku Tree", 0x1C),
							 | 
						||
| 
								 | 
							
								    ("Market", 0x1D),
							 | 
						||
| 
								 | 
							
								    ("Title Theme", 0x1E),
							 | 
						||
| 
								 | 
							
								    ("House", 0x1F),
							 | 
						||
| 
								 | 
							
								    ("Jabu Jabu", 0x26),
							 | 
						||
| 
								 | 
							
								    ("Kakariko Child", 0x27),
							 | 
						||
| 
								 | 
							
								    ("Fairy Fountain", 0x28),
							 | 
						||
| 
								 | 
							
								    ("Zelda Theme", 0x29),
							 | 
						||
| 
								 | 
							
								    ("Fire Temple", 0x2A),
							 | 
						||
| 
								 | 
							
								    ("Forest Temple", 0x2C),
							 | 
						||
| 
								 | 
							
								    ("Castle Courtyard", 0x2D),
							 | 
						||
| 
								 | 
							
								    ("Ganondorf Theme", 0x2E),
							 | 
						||
| 
								 | 
							
								    ("Lon Lon Ranch", 0x2F),
							 | 
						||
| 
								 | 
							
								    ("Goron City", 0x30),
							 | 
						||
| 
								 | 
							
								    ("Miniboss Battle", 0x38),
							 | 
						||
| 
								 | 
							
								    ("Temple of Time", 0x3A),
							 | 
						||
| 
								 | 
							
								    ("Kokiri Forest", 0x3C),
							 | 
						||
| 
								 | 
							
								    ("Lost Woods", 0x3E),
							 | 
						||
| 
								 | 
							
								    ("Spirit Temple", 0x3F),
							 | 
						||
| 
								 | 
							
								    ("Horse Race", 0x40),
							 | 
						||
| 
								 | 
							
								    ("Ingo Theme", 0x42),
							 | 
						||
| 
								 | 
							
								    ("Fairy Flying", 0x4A),
							 | 
						||
| 
								 | 
							
								    ("Deku Tree", 0x4B),
							 | 
						||
| 
								 | 
							
								    ("Windmill Hut", 0x4C),
							 | 
						||
| 
								 | 
							
								    ("Shooting Gallery", 0x4E),
							 | 
						||
| 
								 | 
							
								    ("Sheik Theme", 0x4F),
							 | 
						||
| 
								 | 
							
								    ("Zoras Domain", 0x50),
							 | 
						||
| 
								 | 
							
								    ("Shop", 0x55),
							 | 
						||
| 
								 | 
							
								    ("Chamber of the Sages", 0x56),
							 | 
						||
| 
								 | 
							
								    ("Ice Cavern", 0x58),
							 | 
						||
| 
								 | 
							
								    ("Kaepora Gaebora", 0x5A),
							 | 
						||
| 
								 | 
							
								    ("Shadow Temple", 0x5B),
							 | 
						||
| 
								 | 
							
								    ("Water Temple", 0x5C),
							 | 
						||
| 
								 | 
							
								    ("Gerudo Valley", 0x5F),
							 | 
						||
| 
								 | 
							
								    ("Potion Shop", 0x60),
							 | 
						||
| 
								 | 
							
								    ("Kotake and Koume", 0x61),
							 | 
						||
| 
								 | 
							
								    ("Castle Escape", 0x62),
							 | 
						||
| 
								 | 
							
								    ("Castle Underground", 0x63),
							 | 
						||
| 
								 | 
							
								    ("Ganondorf Battle", 0x64),
							 | 
						||
| 
								 | 
							
								    ("Ganon Battle", 0x65),
							 | 
						||
| 
								 | 
							
								    ("Fire Boss", 0x6B),
							 | 
						||
| 
								 | 
							
								    ("Mini-game", 0x6C)
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								fanfare_sequence_ids = [
							 | 
						||
| 
								 | 
							
								    ("Game Over", 0x20),
							 | 
						||
| 
								 | 
							
								    ("Boss Defeated", 0x21),
							 | 
						||
| 
								 | 
							
								    ("Item Get", 0x22),
							 | 
						||
| 
								 | 
							
								    ("Ganondorf Appears", 0x23),
							 | 
						||
| 
								 | 
							
								    ("Heart Container Get", 0x24),
							 | 
						||
| 
								 | 
							
								    ("Treasure Chest", 0x2B),
							 | 
						||
| 
								 | 
							
								    ("Spirit Stone Get", 0x32),
							 | 
						||
| 
								 | 
							
								    ("Heart Piece Get", 0x39),
							 | 
						||
| 
								 | 
							
								    ("Escape from Ranch", 0x3B),
							 | 
						||
| 
								 | 
							
								    ("Learn Song", 0x3D),
							 | 
						||
| 
								 | 
							
								    ("Epona Race Goal", 0x41),
							 | 
						||
| 
								 | 
							
								    ("Medallion Get", 0x43),
							 | 
						||
| 
								 | 
							
								    ("Zelda Turns Around", 0x51),
							 | 
						||
| 
								 | 
							
								    ("Master Sword", 0x53),
							 | 
						||
| 
								 | 
							
								    ("Door of Time", 0x59)
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								ocarina_sequence_ids = [
							 | 
						||
| 
								 | 
							
								    ("Prelude of Light", 0x25),
							 | 
						||
| 
								 | 
							
								    ("Bolero of Fire", 0x33),
							 | 
						||
| 
								 | 
							
								    ("Minuet of Forest", 0x34),
							 | 
						||
| 
								 | 
							
								    ("Serenade of Water", 0x35),
							 | 
						||
| 
								 | 
							
								    ("Requiem of Spirit", 0x36),
							 | 
						||
| 
								 | 
							
								    ("Nocturne of Shadow", 0x37),
							 | 
						||
| 
								 | 
							
								    ("Saria's Song", 0x44),
							 | 
						||
| 
								 | 
							
								    ("Epona's Song", 0x45),
							 | 
						||
| 
								 | 
							
								    ("Zelda's Lullaby", 0x46),
							 | 
						||
| 
								 | 
							
								    ("Sun's Song", 0x47),
							 | 
						||
| 
								 | 
							
								    ("Song of Time", 0x48),
							 | 
						||
| 
								 | 
							
								    ("Song of Storms", 0x49)
							 | 
						||
| 
								 | 
							
								]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Represents the information associated with a sequence, aside from the sequence data itself
							 | 
						||
| 
								 | 
							
								class TableEntry(object):
							 | 
						||
| 
								 | 
							
								    def __init__(self, name, cosmetic_name, type = 0x0202, instrument_set = 0x03, replaces = -1, vanilla_id = -1):
							 | 
						||
| 
								 | 
							
								        self.name = name
							 | 
						||
| 
								 | 
							
								        self.cosmetic_name = cosmetic_name
							 | 
						||
| 
								 | 
							
								        self.replaces = replaces
							 | 
						||
| 
								 | 
							
								        self.vanilla_id = vanilla_id
							 | 
						||
| 
								 | 
							
								        self.type = type
							 | 
						||
| 
								 | 
							
								        self.instrument_set = instrument_set
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def copy(self):
							 | 
						||
| 
								 | 
							
								        copy = TableEntry(self.name, self.cosmetic_name, self.type, self.instrument_set, self.replaces, self.vanilla_id)
							 | 
						||
| 
								 | 
							
								        return copy
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# Represents actual sequence data, along with metadata for the sequence data block
							 | 
						||
| 
								 | 
							
								class Sequence(object):
							 | 
						||
| 
								 | 
							
								    def __init__(self):
							 | 
						||
| 
								 | 
							
								        self.address = -1
							 | 
						||
| 
								 | 
							
								        self.size = -1
							 | 
						||
| 
								 | 
							
								        self.data = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, ids, seq_type = 'bgm'):
							 | 
						||
| 
								 | 
							
								    # Process vanilla music data
							 | 
						||
| 
								 | 
							
								    for bgm in ids:
							 | 
						||
| 
								 | 
							
								        # Get sequence metadata
							 | 
						||
| 
								 | 
							
								        name = bgm[0]
							 | 
						||
| 
								 | 
							
								        cosmetic_name = name
							 | 
						||
| 
								 | 
							
								        type = rom.read_int16(0xB89AE8 + (bgm[1] * 0x10))
							 | 
						||
| 
								 | 
							
								        instrument_set = rom.read_byte(0xB89911 + 0xDD + (bgm[1] * 2))
							 | 
						||
| 
								 | 
							
								        id = bgm[1]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Create new sequences
							 | 
						||
| 
								 | 
							
								        seq = TableEntry(name, cosmetic_name, type, instrument_set, vanilla_id = id)
							 | 
						||
| 
								 | 
							
								        target = TableEntry(name, cosmetic_name, type, instrument_set, replaces = id)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Special handling for file select/fairy fountain
							 | 
						||
| 
								 | 
							
								        if seq.vanilla_id != 0x57 and cosmetic_name not in disabled_source_sequences:
							 | 
						||
| 
								 | 
							
								            sequences.append(seq)
							 | 
						||
| 
								 | 
							
								        if cosmetic_name not in disabled_target_sequences:
							 | 
						||
| 
								 | 
							
								            target_sequences.append(target)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # If present, load the file containing custom music to exclude
							 | 
						||
| 
								 | 
							
								    try:
							 | 
						||
| 
								 | 
							
								        with open(os.path.join(data_path(), u'custom_music_exclusion.txt')) as excl_in:
							 | 
						||
| 
								 | 
							
								            seq_exclusion_list = excl_in.readlines()
							 | 
						||
| 
								 | 
							
								        seq_exclusion_list = [seq.rstrip() for seq in seq_exclusion_list if seq[0] != '#']
							 | 
						||
| 
								 | 
							
								        seq_exclusion_list = [seq for seq in seq_exclusion_list if seq.endswith('.meta')]
							 | 
						||
| 
								 | 
							
								    except FileNotFoundError:
							 | 
						||
| 
								 | 
							
								        seq_exclusion_list = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Process music data in data/Music/
							 | 
						||
| 
								 | 
							
								    # Each sequence requires a valid .seq sequence file and a .meta metadata file
							 | 
						||
| 
								 | 
							
								    # Current .meta format: Cosmetic Name\nInstrument Set\nPool
							 | 
						||
| 
								 | 
							
								    for dirpath, _, filenames in os.walk(u'./data/Music', followlinks=True):
							 | 
						||
| 
								 | 
							
								        for fname in filenames:
							 | 
						||
| 
								 | 
							
								            # Skip if included in exclusion file
							 | 
						||
| 
								 | 
							
								            if fname in seq_exclusion_list:
							 | 
						||
| 
								 | 
							
								                continue
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            # Find meta file and check if corresponding seq file exists
							 | 
						||
| 
								 | 
							
								            if fname.endswith('.meta') and os.path.isfile(os.path.join(dirpath, fname.split('.')[0] + '.seq')):
							 | 
						||
| 
								 | 
							
								                # Read meta info
							 | 
						||
| 
								 | 
							
								                try:
							 | 
						||
| 
								 | 
							
								                    with open(os.path.join(dirpath, fname), 'r') as stream:
							 | 
						||
| 
								 | 
							
								                        lines = stream.readlines()
							 | 
						||
| 
								 | 
							
								                    # Strip newline(s)
							 | 
						||
| 
								 | 
							
								                    lines = [line.rstrip() for line in lines]
							 | 
						||
| 
								 | 
							
								                except FileNotFoundError as ex:
							 | 
						||
| 
								 | 
							
								                    raise FileNotFoundError('No meta file for: "' + fname + '". This should never happen')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                # Create new sequence, checking third line for correct type
							 | 
						||
| 
								 | 
							
								                if (len(lines) > 2 and (lines[2].lower() == seq_type.lower() or lines[2] == '')) or (len(lines) <= 2 and seq_type == 'bgm'):
							 | 
						||
| 
								 | 
							
								                    seq = TableEntry(os.path.join(dirpath, fname.split('.')[0]), lines[0], instrument_set = int(lines[1], 16))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    if seq.instrument_set < 0x00 or seq.instrument_set > 0x25:
							 | 
						||
| 
								 | 
							
								                        raise Exception('Sequence instrument must be in range [0x00, 0x25]')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    if seq.cosmetic_name not in disabled_source_sequences:
							 | 
						||
| 
								 | 
							
								                        sequences.append(seq)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return sequences, target_sequences
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def shuffle_music(sequences, target_sequences, music_mapping, log):
							 | 
						||
| 
								 | 
							
								    sequence_dict = {}
							 | 
						||
| 
								 | 
							
								    sequence_ids = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for sequence in sequences:
							 | 
						||
| 
								 | 
							
								        if sequence.cosmetic_name == "None":
							 | 
						||
| 
								 | 
							
								            raise Exception('Sequences should not be named "None" as that is used for disabled music. Sequence with improper name: %s' % sequence.name)
							 | 
						||
| 
								 | 
							
								        if sequence.cosmetic_name in sequence_dict:
							 | 
						||
| 
								 | 
							
								            raise Exception('Sequence names should be unique. Duplicate sequence name: %s' % sequence.cosmetic_name)
							 | 
						||
| 
								 | 
							
								        sequence_dict[sequence.cosmetic_name] = sequence
							 | 
						||
| 
								 | 
							
								        if sequence.cosmetic_name not in music_mapping.values():
							 | 
						||
| 
								 | 
							
								            sequence_ids.append(sequence.cosmetic_name)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Shuffle the sequences
							 | 
						||
| 
								 | 
							
								    if len(sequences) < len(target_sequences):
							 | 
						||
| 
								 | 
							
								        raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
							 | 
						||
| 
								 | 
							
								    random.shuffle(sequence_ids)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    sequences = []
							 | 
						||
| 
								 | 
							
								    for target_sequence in target_sequences:
							 | 
						||
| 
								 | 
							
								        sequence = sequence_dict[sequence_ids.pop()].copy() if target_sequence.cosmetic_name not in music_mapping \
							 | 
						||
| 
								 | 
							
								            else ("None", 0x0) if music_mapping[target_sequence.cosmetic_name] == "None" \
							 | 
						||
| 
								 | 
							
								            else sequence_dict[music_mapping[target_sequence.cosmetic_name]].copy()
							 | 
						||
| 
								 | 
							
								        sequences.append(sequence)
							 | 
						||
| 
								 | 
							
								        sequence.replaces = target_sequence.replaces
							 | 
						||
| 
								 | 
							
								        log[target_sequence.cosmetic_name] = sequence.cosmetic_name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return sequences, log
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def rebuild_sequences(rom, sequences):
							 | 
						||
| 
								 | 
							
								    # List of sequences (actual sequence data objects) containing the vanilla sequence data
							 | 
						||
| 
								 | 
							
								    old_sequences = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for i in range(0x6E):
							 | 
						||
| 
								 | 
							
								        # Create new sequence object, an entry for the audio sequence
							 | 
						||
| 
								 | 
							
								        entry = Sequence()
							 | 
						||
| 
								 | 
							
								        # Get the address for the entry's pointer table entry
							 | 
						||
| 
								 | 
							
								        entry_address = 0xB89AE0 + (i * 0x10)
							 | 
						||
| 
								 | 
							
								        # Extract the info from the pointer table entry
							 | 
						||
| 
								 | 
							
								        entry.address = rom.read_int32(entry_address)
							 | 
						||
| 
								 | 
							
								        entry.size = rom.read_int32(entry_address + 0x04)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # If size > 0, read the sequence data from the rom into the sequence object
							 | 
						||
| 
								 | 
							
								        if entry.size > 0:
							 | 
						||
| 
								 | 
							
								            entry.data = rom.read_bytes(entry.address + 0x029DE0, entry.size)
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            s = [seq for seq in sequences if seq.replaces == i]
							 | 
						||
| 
								 | 
							
								            if s != [] and entry.address > 0 and entry.address < 128:
							 | 
						||
| 
								 | 
							
								                s = s.pop()
							 | 
						||
| 
								 | 
							
								                if s.replaces != 0x28:
							 | 
						||
| 
								 | 
							
								                    s.replaces = entry.address
							 | 
						||
| 
								 | 
							
								                else:
							 | 
						||
| 
								 | 
							
								                    # Special handling for file select/fairy fountain
							 | 
						||
| 
								 | 
							
								                    entry.data = old_sequences[0x57].data
							 | 
						||
| 
								 | 
							
								                    entry.size = old_sequences[0x57].size
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        old_sequences.append(entry)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # List of sequences containing the new sequence data
							 | 
						||
| 
								 | 
							
								    new_sequences = []
							 | 
						||
| 
								 | 
							
								    address = 0
							 | 
						||
| 
								 | 
							
								    # Byte array to hold the data for the whole audio sequence
							 | 
						||
| 
								 | 
							
								    new_audio_sequence = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for i in range(0x6E):
							 | 
						||
| 
								 | 
							
								        new_entry = Sequence()
							 | 
						||
| 
								 | 
							
								        # If sequence size is 0, the address doesn't matter and it doesn't effect the current address
							 | 
						||
| 
								 | 
							
								        if old_sequences[i].size == 0:
							 | 
						||
| 
								 | 
							
								            new_entry.address = old_sequences[i].address
							 | 
						||
| 
								 | 
							
								        # Continue from the end of the new sequence table
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            new_entry.address = address
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        s = [seq for seq in sequences if seq.replaces == i]
							 | 
						||
| 
								 | 
							
								        if s != []:
							 | 
						||
| 
								 | 
							
								            assert len(s) == 1
							 | 
						||
| 
								 | 
							
								            s = s.pop()
							 | 
						||
| 
								 | 
							
								            # If we are using a vanilla sequence, get its data from old_sequences
							 | 
						||
| 
								 | 
							
								            if s.vanilla_id != -1:
							 | 
						||
| 
								 | 
							
								                new_entry.size = old_sequences[s.vanilla_id].size
							 | 
						||
| 
								 | 
							
								                new_entry.data = old_sequences[s.vanilla_id].data
							 | 
						||
| 
								 | 
							
								            else:
							 | 
						||
| 
								 | 
							
								                # Read sequence info
							 | 
						||
| 
								 | 
							
								                try:
							 | 
						||
| 
								 | 
							
								                    with open(s.name + '.seq', 'rb') as stream:
							 | 
						||
| 
								 | 
							
								                        new_entry.data = bytearray(stream.read())
							 | 
						||
| 
								 | 
							
								                    new_entry.size = len(new_entry.data)
							 | 
						||
| 
								 | 
							
								                    if new_entry.size <= 0x10:
							 | 
						||
| 
								 | 
							
								                        raise Exception('Invalid sequence file "' + s.name + '.seq"')
							 | 
						||
| 
								 | 
							
								                    new_entry.data[1] = 0x20
							 | 
						||
| 
								 | 
							
								                except FileNotFoundError as ex:
							 | 
						||
| 
								 | 
							
								                    raise FileNotFoundError('No sequence file for: "' + s.name + '"')
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            new_entry.size = old_sequences[i].size
							 | 
						||
| 
								 | 
							
								            new_entry.data = old_sequences[i].data
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        new_sequences.append(new_entry)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Concatenate the full audio sequence and the new sequence data
							 | 
						||
| 
								 | 
							
								        if new_entry.data != [] and new_entry.size > 0:
							 | 
						||
| 
								 | 
							
								            # Align sequences to 0x10
							 | 
						||
| 
								 | 
							
								            if new_entry.size % 0x10 != 0:
							 | 
						||
| 
								 | 
							
								                new_entry.data.extend(bytearray(0x10 - (new_entry.size % 0x10)))
							 | 
						||
| 
								 | 
							
								                new_entry.size += 0x10 - (new_entry.size % 0x10)
							 | 
						||
| 
								 | 
							
								            new_audio_sequence.extend(new_entry.data)
							 | 
						||
| 
								 | 
							
								            # Increment the current address by the size of the new sequence
							 | 
						||
| 
								 | 
							
								            address += new_entry.size
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Check if the new audio sequence is larger than the vanilla one
							 | 
						||
| 
								 | 
							
								    if address > 0x04F690:
							 | 
						||
| 
								 | 
							
								        # Zero out the old audio sequence
							 | 
						||
| 
								 | 
							
								        rom.buffer[0x029DE0 : 0x029DE0 + 0x04F690] = [0] * 0x04F690
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        # Append new audio sequence
							 | 
						||
| 
								 | 
							
								        new_address = rom.free_space()
							 | 
						||
| 
								 | 
							
								        rom.write_bytes(new_address, new_audio_sequence)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #Update dmatable
							 | 
						||
| 
								 | 
							
								        rom.update_dmadata_record(0x029DE0, new_address, new_address + address)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    else:
							 | 
						||
| 
								 | 
							
								        # Write new audio sequence file
							 | 
						||
| 
								 | 
							
								        rom.write_bytes(0x029DE0, new_audio_sequence)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Update pointer table
							 | 
						||
| 
								 | 
							
								    for i in range(0x6E):
							 | 
						||
| 
								 | 
							
								        rom.write_int32(0xB89AE0 + (i * 0x10), new_sequences[i].address)
							 | 
						||
| 
								 | 
							
								        rom.write_int32(0xB89AE0 + (i * 0x10) + 0x04, new_sequences[i].size)
							 | 
						||
| 
								 | 
							
								        s = [seq for seq in sequences if seq.replaces == i]
							 | 
						||
| 
								 | 
							
								        if s != []:
							 | 
						||
| 
								 | 
							
								            assert len(s) == 1
							 | 
						||
| 
								 | 
							
								            s = s.pop()
							 | 
						||
| 
								 | 
							
								            rom.write_int16(0xB89AE0 + (i * 0x10) + 0x08, s.type)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Update instrument sets
							 | 
						||
| 
								 | 
							
								    for i in range(0x6E):
							 | 
						||
| 
								 | 
							
								        base = 0xB89911 + 0xDD + (i * 2)
							 | 
						||
| 
								 | 
							
								        j = -1
							 | 
						||
| 
								 | 
							
								        if new_sequences[i].size == 0:
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                j = [seq for seq in sequences if seq.replaces == new_sequences[i].address].pop()
							 | 
						||
| 
								 | 
							
								            except:
							 | 
						||
| 
								 | 
							
								                j = -1
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            try:
							 | 
						||
| 
								 | 
							
								                j = [seq for seq in sequences if seq.replaces == i].pop()
							 | 
						||
| 
								 | 
							
								            except:
							 | 
						||
| 
								 | 
							
								                j = -1
							 | 
						||
| 
								 | 
							
								        if j != -1:
							 | 
						||
| 
								 | 
							
								            rom.write_byte(base, j.instrument_set)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def shuffle_pointers_table(rom, ids, music_mapping, log):
							 | 
						||
| 
								 | 
							
								    # Read in all the Music data
							 | 
						||
| 
								 | 
							
								    bgm_data = {}
							 | 
						||
| 
								 | 
							
								    bgm_ids = []
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for bgm in ids:
							 | 
						||
| 
								 | 
							
								        bgm_sequence = rom.read_bytes(0xB89AE0 + (bgm[1] * 0x10), 0x10)
							 | 
						||
| 
								 | 
							
								        bgm_instrument = rom.read_int16(0xB89910 + 0xDD + (bgm[1] * 2))
							 | 
						||
| 
								 | 
							
								        bgm_data[bgm[0]] = (bgm[0], bgm_sequence, bgm_instrument)
							 | 
						||
| 
								 | 
							
								        if bgm[0] not in music_mapping.values():
							 | 
						||
| 
								 | 
							
								            bgm_ids.append(bgm[0])
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # shuffle data
							 | 
						||
| 
								 | 
							
								    random.shuffle(bgm_ids)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Write Music data back in random ordering
							 | 
						||
| 
								 | 
							
								    for bgm in ids:
							 | 
						||
| 
								 | 
							
								        if bgm[0] in music_mapping and music_mapping[bgm[0]] in bgm_data:
							 | 
						||
| 
								 | 
							
								            bgm_name = music_mapping[bgm[0]]
							 | 
						||
| 
								 | 
							
								        else:
							 | 
						||
| 
								 | 
							
								            bgm_name = bgm_ids.pop()
							 | 
						||
| 
								 | 
							
								        bgm_name, bgm_sequence, bgm_instrument = bgm_data[bgm_name]
							 | 
						||
| 
								 | 
							
								        rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), bgm_sequence)
							 | 
						||
| 
								 | 
							
								        rom.write_int16(0xB89910 + 0xDD + (bgm[1] * 2), bgm_instrument)
							 | 
						||
| 
								 | 
							
								        log[bgm[0]] = bgm_name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Write Fairy Fountain instrument to File Select (uses same track but different instrument set pointer for some reason)
							 | 
						||
| 
								 | 
							
								    rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), rom.read_int16(0xB89910 + 0xDD + (0x28 * 2)))
							 | 
						||
| 
								 | 
							
								    return log
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def randomize_music(rom, ootworld, music_mapping):
							 | 
						||
| 
								 | 
							
								    log = {}
							 | 
						||
| 
								 | 
							
								    errors = []
							 | 
						||
| 
								 | 
							
								    sequences = []
							 | 
						||
| 
								 | 
							
								    target_sequences = []
							 | 
						||
| 
								 | 
							
								    fanfare_sequences = []
							 | 
						||
| 
								 | 
							
								    fanfare_target_sequences = []
							 | 
						||
| 
								 | 
							
								    disabled_source_sequences = {}
							 | 
						||
| 
								 | 
							
								    disabled_target_sequences = {}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Make sure we aren't operating directly on these.
							 | 
						||
| 
								 | 
							
								    music_mapping = music_mapping.copy()
							 | 
						||
| 
								 | 
							
								    bgm_ids = bgm_sequence_ids.copy()
							 | 
						||
| 
								 | 
							
								    ff_ids = fanfare_sequence_ids.copy()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Check if we have mapped music for BGM, Fanfares, or Ocarina Fanfares
							 | 
						||
| 
								 | 
							
								    bgm_mapped = any(bgm[0] in music_mapping for bgm in bgm_ids)
							 | 
						||
| 
								 | 
							
								    ff_mapped = any(ff[0] in music_mapping for ff in ff_ids)
							 | 
						||
| 
								 | 
							
								    ocarina_mapped = any(ocarina[0] in music_mapping for ocarina in ocarina_sequence_ids)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Include ocarina songs in fanfare pool if checked
							 | 
						||
| 
								 | 
							
								    if ootworld.ocarina_fanfares or ocarina_mapped:
							 | 
						||
| 
								 | 
							
								        ff_ids.extend(ocarina_sequence_ids)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Flag sequence locations that are set to off for disabling.
							 | 
						||
| 
								 | 
							
								    disabled_ids = []
							 | 
						||
| 
								 | 
							
								    if ootworld.background_music == 'off':
							 | 
						||
| 
								 | 
							
								        disabled_ids += [music_id for music_id in bgm_ids]
							 | 
						||
| 
								 | 
							
								    if ootworld.fanfares == 'off':
							 | 
						||
| 
								 | 
							
								        disabled_ids += [music_id for music_id in ff_ids]
							 | 
						||
| 
								 | 
							
								        disabled_ids += [music_id for music_id in ocarina_sequence_ids]
							 | 
						||
| 
								 | 
							
								    for bgm in [music_id for music_id in bgm_ids + ff_ids + ocarina_sequence_ids]:
							 | 
						||
| 
								 | 
							
								        if music_mapping.get(bgm[0], '') == "None":
							 | 
						||
| 
								 | 
							
								            disabled_target_sequences[bgm[0]] = bgm
							 | 
						||
| 
								 | 
							
								    for bgm in disabled_ids:
							 | 
						||
| 
								 | 
							
								        if bgm[0] not in music_mapping:
							 | 
						||
| 
								 | 
							
								            music_mapping[bgm[0]] = "None"
							 | 
						||
| 
								 | 
							
								            disabled_target_sequences[bgm[0]] = bgm
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Map music to itself if music is set to normal.
							 | 
						||
| 
								 | 
							
								    normal_ids = []
							 | 
						||
| 
								 | 
							
								    if ootworld.background_music == 'normal' and bgm_mapped:
							 | 
						||
| 
								 | 
							
								        normal_ids += [music_id for music_id in bgm_ids]
							 | 
						||
| 
								 | 
							
								    if ootworld.fanfares == 'normal' and (ff_mapped or ocarina_mapped):
							 | 
						||
| 
								 | 
							
								        normal_ids += [music_id for music_id in ff_ids]
							 | 
						||
| 
								 | 
							
								    if not ootworld.ocarina_fanfares and ootworld.fanfares == 'normal' and ocarina_mapped:
							 | 
						||
| 
								 | 
							
								        normal_ids += [music_id for music_id in ocarina_sequence_ids]
							 | 
						||
| 
								 | 
							
								    for bgm in normal_ids:
							 | 
						||
| 
								 | 
							
								        if bgm[0] not in music_mapping:
							 | 
						||
| 
								 | 
							
								            music_mapping[bgm[0]] = bgm[0]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # If not creating patch file, shuffle audio sequences. Otherwise, shuffle pointer table
							 | 
						||
| 
								 | 
							
								    # If generating from patch, also do a version check to make sure custom sequences are supported.
							 | 
						||
| 
								 | 
							
								    # custom_sequences_enabled = ootworld.compress_rom != 'Patch'
							 | 
						||
| 
								 | 
							
								    # if ootworld.patch_file != '':
							 | 
						||
| 
								 | 
							
								    #     rom_version_bytes = rom.read_bytes(0x35, 3)
							 | 
						||
| 
								 | 
							
								    #     rom_version = f'{rom_version_bytes[0]}.{rom_version_bytes[1]}.{rom_version_bytes[2]}'
							 | 
						||
| 
								 | 
							
								    #     if compare_version(rom_version, '4.11.13') < 0:
							 | 
						||
| 
								 | 
							
								    #         errors.append("Custom music is not supported by this patch version. Only randomizing vanilla music.")
							 | 
						||
| 
								 | 
							
								    #         custom_sequences_enabled = False
							 | 
						||
| 
								 | 
							
								    # if custom_sequences_enabled:
							 | 
						||
| 
								 | 
							
								    #     if ootworld.background_music in ['random', 'random_custom_only'] or bgm_mapped:
							 | 
						||
| 
								 | 
							
								    #         process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
							 | 
						||
| 
								 | 
							
								    #         if ootworld.background_music == 'random_custom_only':
							 | 
						||
| 
								 | 
							
								    #             sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
							 | 
						||
| 
								 | 
							
								    #         sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #     if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
							 | 
						||
| 
								 | 
							
								    #         process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
							 | 
						||
| 
								 | 
							
								    #         if ootworld.fanfares == 'random_custom_only':
							 | 
						||
| 
								 | 
							
								    #             fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
							 | 
						||
| 
								 | 
							
								    #         fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #     if disabled_source_sequences:
							 | 
						||
| 
								 | 
							
								    #         log = disable_music(rom, disabled_source_sequences.values(), log)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #     rebuild_sequences(rom, sequences + fanfare_sequences)
							 | 
						||
| 
								 | 
							
								    # else:
							 | 
						||
| 
								 | 
							
								    if ootworld.background_music == 'randomized' or bgm_mapped:
							 | 
						||
| 
								 | 
							
								        log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
							 | 
						||
| 
								 | 
							
								        log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
							 | 
						||
| 
								 | 
							
								    # end_else
							 | 
						||
| 
								 | 
							
								    if disabled_target_sequences:
							 | 
						||
| 
								 | 
							
								        log = disable_music(rom, disabled_target_sequences.values(), log)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return log, errors
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def disable_music(rom, ids, log):
							 | 
						||
| 
								 | 
							
								    # First track is no music
							 | 
						||
| 
								 | 
							
								    blank_track = rom.read_bytes(0xB89AE0 + (0 * 0x10), 0x10)
							 | 
						||
| 
								 | 
							
								    for bgm in ids:
							 | 
						||
| 
								 | 
							
								        rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), blank_track)
							 | 
						||
| 
								 | 
							
								        log[bgm[0]] = "None"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return log
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								def restore_music(rom):
							 | 
						||
| 
								 | 
							
								    # Restore all music from original
							 | 
						||
| 
								 | 
							
								    for bgm in bgm_sequence_ids + fanfare_sequence_ids + ocarina_sequence_ids:
							 | 
						||
| 
								 | 
							
								        bgm_sequence = rom.original.read_bytes(0xB89AE0 + (bgm[1] * 0x10), 0x10)
							 | 
						||
| 
								 | 
							
								        rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), bgm_sequence)
							 | 
						||
| 
								 | 
							
								        bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (bgm[1] * 2))
							 | 
						||
| 
								 | 
							
								        rom.write_int16(0xB89910 + 0xDD + (bgm[1] * 2), bgm_instrument)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # restore file select instrument
							 | 
						||
| 
								 | 
							
								    bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (0x57 * 2))
							 | 
						||
| 
								 | 
							
								    rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), bgm_instrument)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # Rebuild audioseq
							 | 
						||
| 
								 | 
							
								    orig_start, orig_end, orig_size = rom.original._get_dmadata_record(0x7470)
							 | 
						||
| 
								 | 
							
								    rom.write_bytes(orig_start, rom.original.read_bytes(orig_start, orig_size))
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    # If Audioseq was relocated
							 | 
						||
| 
								 | 
							
								    start, end, size = rom._get_dmadata_record(0x7470)
							 | 
						||
| 
								 | 
							
								    if start != 0x029DE0:
							 | 
						||
| 
								 | 
							
								        # Zero out old audioseq
							 | 
						||
| 
								 | 
							
								        rom.write_bytes(start, [0] * size)
							 | 
						||
| 
								 | 
							
								        rom.update_dmadata_record(start, orig_start, orig_end)
							 | 
						||
| 
								 | 
							
								
							 |