forked from mirror/Archipelago
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
804 lines
37 KiB
Python
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
|