Files
dockipelago/worlds/dk64/randomizer/Patching/MusicRando.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

804 lines
37 KiB
Python

"""Randomize Music passed from Misc options."""
import gzip
import random
import unicodedata
import js
import json
from io import BytesIO
from zipfile import ZipFile
from randomizer.Enums.Songs import Songs
from randomizer.Enums.SongType import SongType
from randomizer.Enums.SongGroups import SongGroup
from randomizer.Enums.Settings import MusicFilters, WinConditionComplex
from randomizer.Lists.Songs import song_data, song_idx_list
from randomizer.Patching.Patcher import ROM
from randomizer.Settings import Settings
from randomizer.Patching.Library.Generic import IsItemSelected, Overlay
from randomizer.Patching.Library.Assets import getPointerLocation, TableNames
from randomizer.Patching.Library.ASM import writeValue, populateOverlayOffsets, getROMAddress
storage_banks = {
0: 0x8000,
1: 0x11B6,
2: 0x07B6,
3: 0x0156,
}
class GroupData:
"""Class to store information regarding groups."""
def __init__(self, name: str, setting: bool, files: list, names: list, extensions: list, song_type: SongType):
"""Initialize with given parameters."""
self.name = name
self.setting = setting
self.files = files
self.names = names
self.extensions = extensions
self.song_type = song_type
def doesSongLoop(data: bytes) -> bool:
"""Check if song loops."""
byte_list = [x for xi, x in enumerate(data) if xi >= 0x44] # Get byte list, exclude header
for ps in range(len(byte_list) - 3):
if byte_list[ps] == 0xFF and byte_list[ps + 1] == 0x2E and byte_list[ps + 2] == 0x00 and byte_list[ps + 3] == 0xFF:
return True
return False
def isValidSong(data: bytes) -> bool:
"""Check if song is a valid bin."""
if len(data) < 0x44:
return False
if len(data) > 24000:
return False
byte_list = [x for xi, x in enumerate(data) if xi < 4]
return byte_list[0] == 0 and byte_list[1] == 0 and byte_list[2] == 0 and byte_list[3] == 0x44
def getAllAssignedVanillaSongs(settings: Settings) -> dict:
"""Return a dictionary of user-assigned vanilla songs.
The keys and values are both of type Songs.
"""
return {Songs[k]: Songs[v] for k, v in settings.music_selection_dict["vanilla"].items()}
def getAllAssignedCustomSongs(settings: Settings) -> dict:
"""Return a dictionary of user-assigned custom songs.
The keys are of type Songs, and the values are strings.
"""
return {Songs[k]: v for k, v in settings.music_selection_dict["custom"].items()}
def getVanillaSongAssignedToLocation(settings: Settings, location: Songs) -> Songs:
"""Return the vanilla song assigned to the given location."""
assigned_vanilla_songs = getAllAssignedVanillaSongs(settings)
if location in assigned_vanilla_songs:
return assigned_vanilla_songs[location]
else:
return None
def getCustomSongAssignedToLocation(settings: Settings, location: Songs) -> str:
"""Return the custom song assigned to the given location."""
assigned_custom_songs = getAllAssignedCustomSongs(settings)
if location in assigned_custom_songs:
return assigned_custom_songs[location]
else:
return None
def categoriesHaveAssignedSongs(settings: Settings, categories: list[SongType]) -> bool:
"""Return true if the provided categories have assigned songs."""
vanilla_songs = getAllAssignedVanillaSongs(settings)
custom_songs = getAllAssignedCustomSongs(settings)
all_assigned_songs = list(vanilla_songs.keys()) + list(custom_songs.keys())
for song_enum in all_assigned_songs:
song = song_data[song_enum]
if song.type in categories:
return True
return False
TAG_CONVERSION_TABLE = {
# Arg0 = group, Arg1 = is location
"Gloomy": [SongGroup.Gloomy, False],
"Lobbies and Shops": [SongGroup.LobbyShop, True],
"Minigames": [SongGroup.Minigames, True],
"Spawning": [SongGroup.Spawning, True],
"Fights": [SongGroup.Fight, True],
"Happy": [SongGroup.Happy, False],
"Collection": [SongGroup.Collection, True],
"Calm": [SongGroup.Calm, False],
"Interiors": [SongGroup.Interiors, True],
"Exteriors": [SongGroup.Exteriors, True],
}
def parseBinString(val: str) -> str:
"""Attempt to parse a filename for binary files since they do not have a song name."""
removed_folders = val.split("/")[-1]
return removed_folders
def filterSongString(val: str) -> str:
"""Filter newline characters from the string."""
split_string = "".join([x for xi, x in enumerate([*val]) if x != "\n" and xi < 30])
return unicodedata.normalize("NFKD", split_string)
class UploadInfo:
"""Class to store information regarding an uploaded song."""
def __init__(self, push_array: list, length_filter: bool, location_filter: bool, song_type: SongType):
"""Initialize with given variables."""
self.raw_input = push_array[0]
self.name = push_array[1]
self.extension = push_array[2]
passed_name = self.name
if self.extension == ".bin":
passed_name = parseBinString(self.name)
self.name_short = f"Unknown\n{filterSongString(passed_name)}"
self.song_file = self.raw_input
self.zip_file = None
self.song_length = 0
self.referenced_index = None
self.location_tags = []
self.mood_tags = []
if self.extension == ".candy":
self.zip_file = ZipFile(BytesIO(bytes(self.raw_input)))
self.song_file = self.zip_file.open("song.bin").read()
data_json = json.loads(self.zip_file.open("data.json").read())
needed_keys = ["game", "song", "converter", "length", "tags"]
enable_json_data = True
for key in needed_keys:
if key not in list(data_json.keys()):
enable_json_data = False
if enable_json_data:
self.name = f"{data_json['game']} \"{data_json['song']}\" converted by {data_json['converter']}"
self.name_short = (
f"{filterSongString(data_json.get('game_short', data_json.get('game', 'Unknown')))}\n{filterSongString(data_json.get('song_short', data_json.get('song', 'Unknown')))}"
)
self.song_length = data_json["length"]
for tag in data_json["tags"]:
if tag in list(TAG_CONVERSION_TABLE.keys()):
if TAG_CONVERSION_TABLE[tag][1]:
# Location Tag
self.location_tags.append(TAG_CONVERSION_TABLE[tag][0])
else:
# Mood Tag
self.mood_tags.append(TAG_CONVERSION_TABLE[tag][0])
will_filter = False
disable_location_tags = True
if self.extension == ".candy":
if length_filter and (song_type != SongType.BGM):
will_filter = True
elif location_filter and (song_type == SongType.BGM):
will_filter = True
disable_location_tags = False
if len(self.location_tags) == 0 or disable_location_tags:
self.location_tags = [
SongGroup.Fight,
SongGroup.LobbyShop,
SongGroup.Interiors,
SongGroup.Exteriors,
SongGroup.Minigames,
SongGroup.Spawning,
SongGroup.Collection,
]
self.filter = will_filter
self.acceptable = isValidSong(self.song_file)
self.used = False
UNPLACED_SONGS = {}
MAX_LENGTH_DIFFERENCE = 0.4
MAX_LENGTH_OFFSET = 3
GLOBAL_SEARCH_INDEX = 0
USED_INDEXES = []
def pushSongToUnplaced(song: UploadInfo, ref_index: int):
"""Pushes song to unplaced song to the UNPLACED_SONGS dictionary."""
global UNPLACED_SONGS
song.referenced_index = ref_index
for tag in song.location_tags:
if tag not in list(UNPLACED_SONGS.keys()):
UNPLACED_SONGS[tag] = []
UNPLACED_SONGS[tag].append(song)
def isSongWithInLengthRange(vanilla_length: int, proposed_length: int) -> bool:
"""Determine whether song is within range of the vanilla length."""
if vanilla_length == 0 or proposed_length == 0:
return True
min_value = vanilla_length * (1 - MAX_LENGTH_DIFFERENCE)
max_value = ((1 + MAX_LENGTH_DIFFERENCE) * (vanilla_length + MAX_LENGTH_OFFSET)) - MAX_LENGTH_OFFSET
if proposed_length > min_value and proposed_length < max_value:
return True
return False
def writeSongMemory(ROM_COPY: ROM, index: int, value: int):
"""Write song memory to ROM."""
# Specifically, write only the song's storage slot.
offset_dict = populateOverlayOffsets(ROM_COPY)
ram_address = 0x80745658 + (index * 2)
rom_address = getROMAddress(ram_address, Overlay.Static, offset_dict)
ROM_COPY.seek(rom_address)
original_value = int.from_bytes(ROM_COPY.readBytes(2), "big")
original_value &= 0xFFF9
write_slot = (value & 6) >> 1
original_value = original_value | ((write_slot & 3) << 1)
writeValue(ROM_COPY, 0x80745658 + (index * 2), Overlay.Static, original_value, offset_dict)
def writeSongVolume(ROM_COPY: ROM, index: int, song_type: SongType):
"""Write song volume to ROM."""
offset_dict = populateOverlayOffsets(ROM_COPY)
volumes = {
SongType.BGM: 23000,
SongType.Event: 25000,
SongType.MajorItem: 27000,
SongType.MinorItem: 25000,
}
ROM_COPY.seek(TYPE_ARRAY + index)
if song_type in TYPE_VALUES:
ROM_COPY.write(TYPE_VALUES.index(song_type))
else:
ROM_COPY.write(255)
if song_type in volumes:
writeValue(ROM_COPY, 0x807454F0 + (index * 2), Overlay.Static, volumes.get(song_type, 23000), offset_dict)
def getAssignedCustomSongData(file_data_array: list, song_name: str, length_filter: bool, location_filter: bool, song_type: SongType) -> UploadInfo:
"""Request a specific custom song from the list."""
global USED_INDEXES
MAX_SEARCH_LENGTH = len(file_data_array)
# Because all of the user-assigned custom songs get moved to the back of
# the list, it's faster to search through the list backwards.
for i in reversed(range(MAX_SEARCH_LENGTH)):
item = file_data_array[i]
if song_name == item[1]:
USED_INDEXES.append(i)
return UploadInfo(item, length_filter, location_filter, song_type)
# The requested song was not found. (This should never happen due to client-side
# validation.)
raise ValueError(f'Requested non-existent custom song "{song_name}".')
def requestNewSong(
file_data_array: list,
location_tags: list,
location_length: int,
song_type: SongType,
check_unused: bool,
location_filter: bool,
length_filter: bool,
) -> UploadInfo:
"""Request new song from list."""
global GLOBAL_SEARCH_INDEX, USED_INDEXES, UNPLACED_SONGS
# First, filter through the unplaced songs dictionary and remove any used songs
for tag in UNPLACED_SONGS:
UNPLACED_SONGS[tag] = [x for x in UNPLACED_SONGS[tag] if not x.used]
check_tag = song_type == SongType.BGM
# Next, check songs that have already been gone through.
# If one of them satisfies the conditions, then we can just use that one and pop it from the list
if check_tag:
# Tag-Based Search
loc_tag_copy = location_tags.copy()
random.shuffle(loc_tag_copy)
for tag in loc_tag_copy:
if tag in list(UNPLACED_SONGS.keys()):
if len(UNPLACED_SONGS[tag]) > 0:
found_song = UNPLACED_SONGS[tag].pop(0)
found_song.used = check_unused
USED_INDEXES.append(found_song.referenced_index)
return found_song
else:
# Length-Based Search
for tag in UNPLACED_SONGS:
songs_in_tag = UNPLACED_SONGS[tag]
if len(songs_in_tag) > 0:
for si, song in enumerate(songs_in_tag):
if isSongWithInLengthRange(location_length, song.song_length):
found_song = UNPLACED_SONGS[tag].pop(si)
found_song.used = check_unused
USED_INDEXES.append(found_song.referenced_index)
return found_song
# Otherwise, go through the list of ones we're yet to go through
MAX_SEARCH_LENGTH = len(file_data_array)
start_index = GLOBAL_SEARCH_INDEX
for i in range(MAX_SEARCH_LENGTH):
GLOBAL_SEARCH_INDEX = (start_index + i) % MAX_SEARCH_LENGTH
referenced_index = GLOBAL_SEARCH_INDEX
item = UploadInfo(file_data_array[GLOBAL_SEARCH_INDEX], length_filter, location_filter, song_type)
GLOBAL_SEARCH_INDEX = (start_index + i + 1) % MAX_SEARCH_LENGTH # Advance 1 just in case we return from this
if referenced_index in USED_INDEXES and check_unused:
continue
if item.acceptable:
if not item.filter:
if item.acceptable:
USED_INDEXES.append(referenced_index)
return item
elif not check_tag:
if isSongWithInLengthRange(location_length, item.song_length):
USED_INDEXES.append(referenced_index)
return item
# Not similar enough, push to dictionary
pushSongToUnplaced(item, referenced_index)
else:
loc_set = set(location_tags)
song_set = set(item.location_tags)
if loc_set & song_set: # Has a tag in common
USED_INDEXES.append(referenced_index)
return item
# No matches, push to dictionary
pushSongToUnplaced(item, referenced_index)
return None
def insertUploaded(
ROM_COPY: ROM,
settings: Settings,
uploaded_songs: list,
uploaded_song_names: list,
uploaded_song_extensions: list,
target_type: SongType,
):
"""Insert uploaded songs into ROM."""
global UNPLACED_SONGS, GLOBAL_SEARCH_INDEX, USED_INDEXES
# Initial Global Variables
UNPLACED_SONGS = {}
GLOBAL_SEARCH_INDEX = 0
USED_INDEXES = []
file_data = list(zip(uploaded_songs, uploaded_song_names, uploaded_song_extensions))
random.shuffle(file_data)
# Initial temporary variables
all_target_songs = [song_enum for song_enum, song in song_data.items() if song.type == target_type]
# Calculate Proportion, add songs if necessary
if settings.custom_music_proportion == "":
settings.custom_music_proportion = 100
proportion = int(settings.custom_music_proportion) / 100
if proportion < 0:
proportion = 0
elif proportion > 1:
proportion = 1
swap_amount = len(file_data)
# Calculate Cap
cap = int(len(all_target_songs) * proportion)
if settings.fill_with_custom_music:
swap_amount = cap
if swap_amount > cap:
swap_amount = cap
# Process user-selected songs
custom_song_locations = []
custom_song_names = set()
vanilla_song_locations = []
for song_location in getAllAssignedVanillaSongs(settings).keys():
if song_location not in all_target_songs:
continue
vanilla_song_locations.append(song_location)
for song_location, song_name in getAllAssignedCustomSongs(settings).items():
if song_location not in all_target_songs:
continue
custom_song_locations.append(song_location)
custom_song_names.add(song_name)
# Remove all assigned locations from the target songs, if any.
available_target_songs = [x for x in all_target_songs if x not in custom_song_locations and x not in vanilla_song_locations]
swap_amount -= len(custom_song_locations)
# Move all user-assigned custom songs to the back of the list. This will
# mitigate the chances of these songs being assigned multiple times.
songs_to_relocate = [item for item in file_data if item[1] in custom_song_names]
file_data = [item for item in file_data if item[1] not in custom_song_names]
file_data.extend(songs_to_relocate)
# Assign random locations from unassigned songs.
songs_to_be_replaced = []
if swap_amount > 0:
try:
songs_to_be_replaced = random.sample(available_target_songs, swap_amount)
except ValueError:
# Too many vanilla songs have been placed to hit the requested
# proportion. Just fill all possible locations.
songs_to_be_replaced = available_target_songs
# Add assigned custom songs back as locations.
songs_to_be_replaced.extend(custom_song_locations)
length_filter = IsItemSelected(settings.music_filtering, settings.music_filtering_selected, MusicFilters.length)
location_filter = IsItemSelected(settings.music_filtering, settings.music_filtering_selected, MusicFilters.location)
# Place Songs
for song_enum in songs_to_be_replaced:
song = song_data[song_enum]
selected_bank = None
assigned_song_name = getCustomSongAssignedToLocation(settings, song_enum)
satisfied = False
if assigned_song_name is not None:
new_song = getAssignedCustomSongData(file_data, assigned_song_name, length_filter, location_filter, target_type)
satisfied = new_song.acceptable
if not satisfied:
new_song = requestNewSong(
file_data,
song.location_tags,
song.song_length,
target_type,
not settings.fill_with_custom_music,
location_filter,
length_filter,
)
selected_cap = 0xFFFFFF
if new_song is not None:
new_song_data = bytes(new_song.song_file)
for bank in storage_banks:
if len(new_song_data) <= storage_banks[bank]: # Song can fit in bank
if selected_cap > storage_banks[bank]: # Bank size is new lowest that fits
selected_bank = bank
selected_cap = storage_banks[bank]
if selected_bank is not None:
old_bank = (song.memory >> 1) & 3
if old_bank < selected_bank:
selected_bank = old_bank # If vanilla bank is bigger, use the vanilla bank
# Construct new memory data based on variables
song.memory &= 0xFEF9
song.memory |= (selected_bank & 3) << 1
loop = doesSongLoop(new_song_data)
loop_val = 0
if loop:
loop_val = 1
song.memory |= loop_val << 8
# Write Song
song.output_name = new_song.name
song.output_name_short = new_song.name_short
song.shuffled = True
ROM_COPY.seek(getPointerLocation(TableNames.MusicMIDI, song.mem_idx))
zipped_data = gzip.compress(new_song_data, compresslevel=9)
ROM_COPY.writeBytes(zipped_data)
ENABLE_CHAOS = False # Enable DK Rap everywhere
TYPE_ARRAY = 0x1FEE200
TYPE_VALUES = [SongType.BGM, SongType.Event, SongType.MajorItem, SongType.MinorItem]
def randomize_music(settings: Settings, ROM_COPY: ROM):
"""Randomize music passed from the misc music settings.
Args:
settings (Settings): Settings object from the windows form.
"""
music_data = {"music_bgm_data": {}, "music_majoritem_data": {}, "music_minoritem_data": {}, "music_event_data": {}}
music_names = [None] * 175
if js.document.getElementById("override_cosmetics").checked or True:
if js.document.getElementById("random_music").checked:
settings.music_bgm_randomized = True
settings.music_majoritems_randomized = True
settings.music_minoritems_randomized = True
settings.music_events_randomized = True
else:
settings.music_bgm_randomized = js.document.getElementById("music_bgm_randomized").checked
settings.music_majoritems_randomized = js.document.getElementById("music_majoritems_randomized").checked
settings.music_minoritems_randomized = js.document.getElementById("music_minoritems_randomized").checked
settings.music_events_randomized = js.document.getElementById("music_events_randomized").checked
else:
if settings.random_music:
settings.music_bgm_randomized = True
settings.music_majoritems_randomized = True
settings.music_minoritems_randomized = True
settings.music_events_randomized = True
NON_BGM_DATA = [
# Minor Items
GroupData("Minor Items", settings.music_minoritems_randomized, None, None, None, SongType.MinorItem),
# Major Items
GroupData("Major Items", settings.music_majoritems_randomized, None, None, None, SongType.MajorItem),
# Events
GroupData("Events", settings.music_events_randomized, None, None, None, SongType.Event),
]
if js.cosmetics is not None and js.cosmetic_names is not None and js.cosmetic_extensions is not None:
NON_BGM_DATA = [
# Minor Items
GroupData(
"Minor Items",
settings.music_minoritems_randomized,
js.cosmetics.minoritems,
js.cosmetic_names.minoritems,
js.cosmetic_extensions.minoritems,
SongType.MinorItem,
),
# Major Items
GroupData(
"Major Items",
settings.music_majoritems_randomized,
js.cosmetics.majoritems,
js.cosmetic_names.majoritems,
js.cosmetic_extensions.majoritems,
SongType.MajorItem,
),
# Events
GroupData(
"Events",
settings.music_events_randomized,
js.cosmetics.events,
js.cosmetic_names.events,
js.cosmetic_extensions.events,
SongType.Event,
),
]
if (
settings.music_bgm_randomized
or settings.music_events_randomized
or settings.music_majoritems_randomized
or settings.music_minoritems_randomized
or categoriesHaveAssignedSongs(settings, TYPE_VALUES)
):
sav = settings.rom_data
ROM_COPY.seek(sav + 0x12E)
ROM_COPY.write(1)
# Read in all vanilla song data from the ROM to preserve it, because some
# song slots may be overwritten by custom music.
song_rom_data = {}
for song in song_data.values():
# Skip "Silence".
if song.mem_idx == 0:
continue
song_info = js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx]
ROM_COPY.seek(song_info["pointing_to"])
rom_data = ROM_COPY.readBytes(song_info["compressed_size"])
uncompressed_data_table = js.pointer_addresses[TableNames.UncompressedFileSizes]["entries"][0]
ROM_COPY.seek(uncompressed_data_table["pointing_to"] + (4 * song.mem_idx))
song_size = ROM_COPY.readBytes(4)
song_rom_data[song.mem_idx] = {"name": song.name, "data": rom_data, "size": song_size, "memory": song.memory}
for song in song_data.values():
song.Reset()
writeSongVolume(ROM_COPY, song.mem_idx, song.type)
if settings.win_condition_item == WinConditionComplex.dk_rap_items:
song_data[Songs.DKRap].type = SongType.Protected # Protect the rap, used for end seq
# Check if we have anything beyond default set for BGM
if settings.music_bgm_randomized or categoriesHaveAssignedSongs(settings, [SongType.BGM]):
# If the user selected standard rando
if not ENABLE_CHAOS:
if js.cosmetics is not None and js.cosmetic_names is not None and js.cosmetic_extensions is not None:
# If uploaded, replace some songs with the uploaded songs
if settings.music_bgm_randomized:
# Insert all of the custom songs.
insertUploaded(
ROM_COPY,
settings,
list(js.cosmetics.bgm),
list(js.cosmetic_names.bgm),
list(js.cosmetic_extensions.bgm),
SongType.BGM,
)
else:
# Only insert the assigned songs.
assigned_songs = []
assigned_names = []
assigned_extensions = []
for song, name, extension in zip(js.cosmetics.bgm, js.cosmetic_names.bgm, js.cosmetic_extensions.bgm):
if name in getAllAssignedCustomSongs(settings).values():
assigned_songs.append(song)
assigned_names.append(name)
assigned_extensions.append(extension)
insertUploaded(ROM_COPY, settings, assigned_songs, assigned_names, assigned_extensions, SongType.BGM)
# Generate the list of BGM songs
song_list = []
pre_shuffled_songs = []
assigned_songs = []
assigned_locations = []
for channel_index in range(12):
song_list.append([])
pre_shuffled_songs.append([])
assigned_songs.append([])
assigned_locations.append([])
# Assign all of the user-specified songs.
if categoriesHaveAssignedSongs(settings, [SongType.BGM]):
for song_location, song in song_data.items():
if song.type != SongType.BGM or song.shuffled:
continue
assigned_song_enum = getVanillaSongAssignedToLocation(settings, song_location)
if assigned_song_enum is not None:
# The location is given the channel of the song
# replacing it. (At the moment, they should always be
# the same.)
assigned_song = song_data[assigned_song_enum]
assigned_songs[assigned_song.channel - 1].append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][assigned_song.mem_idx])
assigned_locations[assigned_song.channel - 1].append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
for song in song_data.values():
if song.type == SongType.BGM:
# For testing, flip these two lines
# song_list.append(pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
if song.shuffled:
pre_shuffled_songs[song.channel - 1].append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
else:
song_list[song.channel - 1].append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
for channel_index in range(12):
# Remove assigned locations.
open_locations = [x for x in song_list[channel_index] if x not in assigned_locations[channel_index]]
# If we're keeping vanilla songs in vanilla locations, do not
# shuffle this list.
if settings.music_bgm_randomized and not settings.music_vanilla_locations:
shuffled_music = song_list[channel_index].copy()
random.shuffle(shuffled_music)
# Move assigned songs to the back of the list, and shorten
# to match open_locations.
pre_assigned_songs = [x for x in shuffled_music if x in assigned_songs[channel_index]]
open_songs = [x for x in shuffled_music if x not in assigned_songs[channel_index]] + pre_assigned_songs
open_songs = open_songs[: len(open_locations)]
else:
# We want all non-assigned, non-shuffled songs to be the
# same as their locations.
open_songs = open_locations.copy()
location_pool = open_locations + assigned_locations[channel_index] + pre_shuffled_songs[channel_index].copy()
song_pool = open_songs + assigned_songs[channel_index] + pre_shuffled_songs[channel_index].copy()
shuffle_music(ROM_COPY, settings, music_data, music_names, location_pool, song_pool, song_rom_data)
# If the user was a poor sap and selected chaos put DK rap for everything
# Don't assign songs, the user must learn from their mistake
else:
# Find the DK rap in the list
rap_song_data = song_data[Songs.DKRap]
rap = js.pointer_addresses[TableNames.MusicMIDI]["entries"][rap_song_data.mem_idx]
# Find all BGM songs
song_list = []
for song in song_data.values():
if song.type == SongType.BGM:
song_list.append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
# Load the DK Rap song data
ROM_COPY.seek(rap["pointing_to"])
stored_data = ROM_COPY.readBytes(rap["compressed_size"])
uncompressed_data_table = js.pointer_addresses[TableNames.UncompressedFileSizes]["entries"][0]
# Replace all songs as the DK rap
for song in song_list:
ROM_COPY.seek(song["pointing_to"])
ROM_COPY.writeBytes(stored_data)
# Update the uncompressed data table to have our new size.
ROM_COPY.seek(uncompressed_data_table["pointing_to"] + (4 * song_list.index(song)))
new_bytes = ROM_COPY.readBytes(4)
ROM_COPY.seek(uncompressed_data_table["pointing_to"] + (4 * song_list.index(rap)))
ROM_COPY.writeBytes(new_bytes)
# Update data
writeSongMemory(ROM_COPY, song["index"], rap_song_data.memory)
for type_data in NON_BGM_DATA:
if type_data.setting or categoriesHaveAssignedSongs(settings, [type_data.song_type]): # If the user wants to randomize the group
if js.cosmetics is not None and js.cosmetic_names is not None and js.cosmetic_extensions is not None:
# If uploaded, replace some songs with the uploaded songs
if type_data.setting:
# Insert all of the custom songs.
insertUploaded(
ROM_COPY,
settings,
list(type_data.files),
list(type_data.names),
list(type_data.extensions),
type_data.song_type,
)
else:
# Only insert the assigned songs.
assigned_songs = []
assigned_names = []
assigned_extensions = []
for song, name, extension in zip(list(type_data.files), list(type_data.names), list(type_data.extensions)):
if name in getAllAssignedCustomSongs(settings).values():
assigned_songs.append(song)
assigned_names.append(name)
assigned_extensions.append(extension)
insertUploaded(ROM_COPY, settings, assigned_songs, assigned_names, assigned_extensions, type_data.song_type)
# Load the list of items in that group
group_items = []
shuffled_group_items = []
assigned_items = []
assigned_item_locations = []
# Assign all of the user-specified songs.
if categoriesHaveAssignedSongs(settings, [type_data.song_type]):
for song_location, song in song_data.items():
if song.type != type_data.song_type:
continue
assigned_song_enum = getVanillaSongAssignedToLocation(settings, song_location)
if assigned_song_enum is not None:
assigned_item_locations.append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
assigned_item = song_data[assigned_song_enum]
assigned_items.append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][assigned_item.mem_idx])
for song in song_data.values():
if song.type == type_data.song_type:
if song.shuffled:
shuffled_group_items.append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
else:
group_items.append(js.pointer_addresses[TableNames.MusicMIDI]["entries"][song.mem_idx])
# Remove assigned locations.
open_locations = [x for x in group_items if x not in assigned_item_locations]
# If we're keeping vanilla songs in vanilla locations, do not
# shuffle this list.
if type_data.setting and not settings.music_vanilla_locations:
# Shuffle the group list
shuffled_music = group_items.copy()
random.shuffle(shuffled_music)
# Move assigned songs to the back of the list, and shorten
# to match open_locations.
pre_assigned_songs = [x for x in shuffled_music if x in assigned_items]
open_songs = [x for x in shuffled_music if x not in assigned_items] + pre_assigned_songs
open_songs = open_songs[: len(open_locations)]
else:
# We want all non-assigned, non-shuffled songs to be the
# same as their locations.
open_songs = open_locations.copy()
location_pool = open_locations + assigned_item_locations + shuffled_group_items.copy()
song_pool = open_songs + assigned_items + shuffled_group_items.copy()
shuffle_music(ROM_COPY, settings, music_data, music_names, location_pool, song_pool, song_rom_data)
return music_data, music_names
def shuffle_music(ROM_COPY: ROM, settings, music_data, music_names, pool_to_shuffle, shuffled_list, song_rom_data):
"""Shuffle the music pool based on the OG list and the shuffled list.
Args:
pool_to_shuffle (list): Original pool to shuffle.
shuffled_list (list): Shuffled order list.
song_rom_data (dict): The original song data from the ROM.
"""
uncompressed_data_table = js.pointer_addresses[TableNames.UncompressedFileSizes]["entries"][0]
stored_song_data = {}
stored_song_sizes = {}
# For each song in the shuffled list, randomize it into the pool using the shuffled list as a base
# First loop over all songs to read data from ROM
for song in pool_to_shuffle:
ROM_COPY.seek(song["pointing_to"])
stored_data = ROM_COPY.readBytes(song["compressed_size"])
stored_song_data[song["index"]] = stored_data
# Update the uncompressed data table to have our new size.
ROM_COPY.seek(uncompressed_data_table["pointing_to"] + (4 * song["index"]))
new_bytes = ROM_COPY.readBytes(4)
stored_song_sizes[song["index"]] = new_bytes
for song, shuffled_song in zip(pool_to_shuffle, shuffled_list):
# If we are inserting an assigned vanilla song, we should write the
# data from the stored ROM data we read earlier. In every other case,
# we will pull the data from whatever is currently stored in this
# song's slot in the ROM.
song_enum = Songs(song["index"])
originalIndex = song["index"]
shuffledIndex = shuffled_song["index"]
if song_enum in getAllAssignedVanillaSongs(settings):
songs = song_rom_data[shuffled_song["index"]]["data"]
song_name = song_rom_data[shuffled_song["index"]]["name"]
song_short_name = song_name
song_size = song_rom_data[shuffled_song["index"]]["size"]
song_memory = song_rom_data[shuffled_song["index"]]["memory"]
else:
songs = stored_song_data[shuffled_song["index"]]
song_name = song_idx_list[shuffledIndex].output_name
song_short_name = song_idx_list[shuffledIndex].output_name_short
song_size = stored_song_sizes[shuffled_song["index"]]
song_memory = song_idx_list[shuffledIndex].memory
ROM_COPY.seek(song["pointing_to"])
ROM_COPY.writeBytes(songs)
# Update the uncompressed data table to have our new size.
ROM_COPY.seek(uncompressed_data_table["pointing_to"] + (4 * song["index"]))
ROM_COPY.writeBytes(song_size)
writeSongMemory(ROM_COPY, originalIndex, song_memory)
music_names[originalIndex] = song_short_name
if song_idx_list[originalIndex].type == SongType.BGM:
music_data["music_bgm_data"][song_idx_list[originalIndex].name] = song_name
elif song_idx_list[originalIndex].type == SongType.MajorItem:
music_data["music_majoritem_data"][song_idx_list[originalIndex].name] = song_name
elif song_idx_list[originalIndex].type == SongType.MinorItem:
music_data["music_minoritem_data"][song_idx_list[originalIndex].name] = song_name
elif song_idx_list[originalIndex].type == SongType.Event:
music_data["music_event_data"][song_idx_list[originalIndex].name] = song_name