Muse Dash: Fix bad generations occuring due to changing item ids (#2122)

This commit is contained in:
Justus Lind 2023-08-30 04:58:34 +10:00 committed by GitHub
parent aa19a79d26
commit 9d29c6d301
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 99 deletions

View File

@ -8,10 +8,9 @@ class SongData(NamedTuple):
code: Optional[int] code: Optional[int]
song_is_free: bool song_is_free: bool
streamer_mode: bool streamer_mode: bool
easy: str = Optional[int] easy: Optional[int]
hard: int = Optional[int] hard: Optional[int]
master: int = Optional[int] master: Optional[int]
secret: int = Optional[int]
class AlbumData(NamedTuple): class AlbumData(NamedTuple):

View File

@ -10,21 +10,22 @@ def load_text_file(name: str) -> str:
class MuseDashCollections: class MuseDashCollections:
"""Contains all the data of Muse Dash, loaded from MuseDashData.txt.""" """Contains all the data of Muse Dash, loaded from MuseDashData.txt."""
STARTING_CODE = 2900000
MUSIC_SHEET_NAME: str = "Music Sheet" MUSIC_SHEET_NAME: str = "Music Sheet"
MUSIC_SHEET_CODE: int MUSIC_SHEET_CODE: int = STARTING_CODE
FREE_ALBUMS = [ FREE_ALBUMS = [
"Default Music", "Default Music",
"Budget Is Burning: Nano Core", "Budget Is Burning: Nano Core",
"Budget is Burning Vol.1" "Budget Is Burning Vol.1",
] ]
DIFF_OVERRIDES = [ DIFF_OVERRIDES = [
"MuseDash ka nanika hi", "MuseDash ka nanika hi",
"Rush-Hour", "Rush-Hour",
"Find this Month's Featured Playlist", "Find this Month's Featured Playlist",
"PeroPero in the Universe" "PeroPero in the Universe",
] ]
album_items: Dict[str, AlbumData] = {} album_items: Dict[str, AlbumData] = {}
@ -33,47 +34,43 @@ class MuseDashCollections:
song_locations: Dict[str, int] = {} song_locations: Dict[str, int] = {}
vfx_trap_items: Dict[str, int] = { vfx_trap_items: Dict[str, int] = {
"Bad Apple Trap": 1, "Bad Apple Trap": STARTING_CODE + 1,
"Pixelate Trap": 2, "Pixelate Trap": STARTING_CODE + 2,
"Random Wave Trap": 3, "Random Wave Trap": STARTING_CODE + 3,
"Shadow Edge Trap": 4, "Shadow Edge Trap": STARTING_CODE + 4,
"Chromatic Aberration Trap": 5, "Chromatic Aberration Trap": STARTING_CODE + 5,
"Background Freeze Trap": 6, "Background Freeze Trap": STARTING_CODE + 6,
"Gray Scale Trap": 7, "Gray Scale Trap": STARTING_CODE + 7,
} }
sfx_trap_items: Dict[str, int] = { sfx_trap_items: Dict[str, int] = {
"Nyaa SFX Trap": 8, "Nyaa SFX Trap": STARTING_CODE + 8,
"Error SFX Trap": 9, "Error SFX Trap": STARTING_CODE + 9,
} }
item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items) item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items)
location_names_to_id = ChainMap(song_locations, album_locations) location_names_to_id = ChainMap(song_locations, album_locations)
def __init__(self, start_item_id: int, items_per_location: int): def __init__(self) -> None:
self.MUSIC_SHEET_CODE = start_item_id
self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE
self.vfx_trap_items.update({k: (v + start_item_id) for (k, v) in self.vfx_trap_items.items()}) item_id_index = self.STARTING_CODE + 50
self.sfx_trap_items.update({k: (v + start_item_id) for (k, v) in self.sfx_trap_items.items()})
item_id_index = start_item_id + 50
location_id_index = start_item_id
full_file = load_text_file("MuseDashData.txt") full_file = load_text_file("MuseDashData.txt")
seen_albums = set()
for line in full_file.splitlines(): for line in full_file.splitlines():
line = line.strip() line = line.strip()
sections = line.split("|") sections = line.split("|")
if sections[2] not in self.album_items: album = sections[2]
self.album_items[sections[2]] = AlbumData(item_id_index) if album not in seen_albums:
seen_albums.add(album)
self.album_items[album] = AlbumData(item_id_index)
item_id_index += 1 item_id_index += 1
# Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff' # Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff'
song_name = sections[0] song_name = sections[0]
# [1] is used in the client copy to make sure item id's match. # [1] is used in the client copy to make sure item id's match.
song_is_free = sections[2] in self.FREE_ALBUMS song_is_free = album in self.FREE_ALBUMS
steamer_mode = sections[3] == "True" steamer_mode = sections[3] == "True"
if song_name in self.DIFF_OVERRIDES: if song_name in self.DIFF_OVERRIDES:
@ -94,17 +91,16 @@ class MuseDashCollections:
self.item_names_to_id.update({name: data.code for name, data in self.song_items.items()}) self.item_names_to_id.update({name: data.code for name, data in self.song_items.items()})
self.item_names_to_id.update({name: data.code for name, data in self.album_items.items()}) self.item_names_to_id.update({name: data.code for name, data in self.album_items.items()})
location_id_index = self.STARTING_CODE
for name in self.album_items.keys(): for name in self.album_items.keys():
for i in range(0, items_per_location): self.album_locations[f"{name}-0"] = location_id_index
new_name = f"{name}-{i}" self.album_locations[f"{name}-1"] = location_id_index + 1
self.album_locations[new_name] = location_id_index location_id_index += 2
location_id_index += 1
for name in self.song_items.keys(): for name in self.song_items.keys():
for i in range(0, items_per_location): self.song_locations[f"{name}-0"] = location_id_index
new_name = f"{name}-{i}" self.song_locations[f"{name}-1"] = location_id_index + 1
self.song_locations[new_name] = location_id_index location_id_index += 2
location_id_index += 1
def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool, def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
diff_lower: int, diff_higher: int) -> List[str]: diff_lower: int, diff_higher: int) -> List[str]:

View File

@ -465,3 +465,7 @@ Kawai Splendid Space Thief|64-4|COSMIC RADIO PEROLIST|False|6|8|10|11
Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8| Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8|
Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10| Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10|
mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11 mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11
BrainDance|65-0|Neon Abyss|True|3|6|9|
My Focus!|65-1|Neon Abyss|True|5|7|10|
ABABABA BURST|65-2|Neon Abyss|True|5|7|9|
ULTRA HIGHER|65-3|Neon Abyss|True|4|7|10|

View File

@ -40,14 +40,14 @@ class MuseDashWorld(World):
game = "Muse Dash" game = "Muse Dash"
option_definitions = musedash_options option_definitions = musedash_options
topology_present = False topology_present = False
data_version = 8 data_version = 9
web = MuseDashWebWorld() web = MuseDashWebWorld()
# Necessary Data # Necessary Data
md_collection = MuseDashCollections(2900000, 2) md_collection = MuseDashCollections()
item_name_to_id = md_collection.item_names_to_id item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
location_name_to_id = md_collection.location_names_to_id location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
# Working Data # Working Data
victory_song_name: str = "" victory_song_name: str = ""
@ -167,11 +167,12 @@ class MuseDashWorld(World):
if trap: if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player) return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
song = self.md_collection.song_items.get(name) album = self.md_collection.album_items.get(name)
if song: if album:
return MuseDashSongItem(name, self.player, song) return MuseDashSongItem(name, self.player, album)
return MuseDashFixedItem(name, ItemClassification.filler, None, self.player) song = self.md_collection.song_items.get(name)
return MuseDashSongItem(name, self.player, song)
def create_items(self) -> None: def create_items(self) -> None:
song_keys_in_pool = self.included_songs.copy() song_keys_in_pool = self.included_songs.copy()

View File

@ -0,0 +1,49 @@
import unittest
from ..MuseDashCollection import MuseDashCollections
class CollectionsTest(unittest.TestCase):
REMOVED_SONGS = [
"CHAOS Glitch",
"FM 17314 SUGAR RADIO",
]
def test_all_names_are_ascii(self) -> None:
bad_names = list()
collection = MuseDashCollections()
for name in collection.song_items.keys():
for c in name:
# This is taken directly from OoT. Represents the generally excepted characters.
if (0x20 <= ord(c) < 0x7e):
continue
bad_names.append(name)
break
self.assertEqual(len(bad_names), 0, f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}")
def test_ids_dont_change(self) -> None:
collection = MuseDashCollections()
itemsBefore = {name: code for name, code in collection.item_names_to_id.items()}
locationsBefore = {name: code for name, code in collection.location_names_to_id.items()}
collection.__init__()
itemsAfter = {name: code for name, code in collection.item_names_to_id.items()}
locationsAfter = {name: code for name, code in collection.location_names_to_id.items()}
self.assertDictEqual(itemsBefore, itemsAfter, "Item ID changed after secondary init.")
self.assertDictEqual(locationsBefore, locationsAfter, "Location ID changed after secondary init.")
def test_free_dlc_included_in_base_songs(self) -> None:
collection = MuseDashCollections()
songs = collection.get_songs_with_settings(False, False, 0, 11)
self.assertIn("Glimmer", songs, "Budget Is Burning Vol.1 is not being included in base songs")
self.assertIn("Out of Sense", songs, "Budget Is Burning: Nano Core is not being included in base songs")
def test_remove_songs_are_not_generated(self) -> None:
collection = MuseDashCollections()
songs = collection.get_songs_with_settings(True, False, 0, 11)
for song_name in self.REMOVED_SONGS:
self.assertNotIn(song_name, songs, f"Song '{song_name}' wasn't removed correctly.")

View File

@ -9,8 +9,8 @@ class DifficultyRanges(MuseDashTestBase):
difficulty_max = self.multiworld.song_difficulty_max[1] difficulty_max = self.multiworld.song_difficulty_max[1]
def test_range(inputRange, lower, upper): def test_range(inputRange, lower, upper):
assert inputRange[0] == lower and inputRange[1] == upper, \ self.assertEqual(inputRange[0], lower)
f"Output incorrect. Got: {inputRange[0]} to {inputRange[1]}. Expected: {lower} to {upper}" self.assertEqual(inputRange[1], upper)
songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1]) songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1])
for songKey in songs: for songKey in songs:
@ -24,7 +24,7 @@ class DifficultyRanges(MuseDashTestBase):
if (song.master is not None and inputRange[0] <= song.master <= inputRange[1]): if (song.master is not None and inputRange[0] <= song.master <= inputRange[1]):
continue continue
assert False, f"Invalid song '{songKey}' was given for range '{inputRange[0]} to {inputRange[1]}'" self.fail(f"Invalid song '{songKey}' was given for range '{inputRange[0]} to {inputRange[1]}'")
#auto ranges #auto ranges
difficulty_choice.value = 0 difficulty_choice.value = 0
@ -65,5 +65,5 @@ class DifficultyRanges(MuseDashTestBase):
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES: for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name] song = muse_dash_world.md_collection.song_items[song_name]
assert song.easy is not None and song.hard is not None and song.master is not None, \ self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
f"Song '{song_name}' difficulty not set when it should be." f"Song '{song_name}' difficulty not set when it should be.")

View File

@ -1,18 +0,0 @@
import unittest
from ..MuseDashCollection import MuseDashCollections
class NamesTest(unittest.TestCase):
def test_all_names_are_ascii(self) -> None:
bad_names = list()
collection = MuseDashCollections(0, 1)
for name in collection.song_items.keys():
for c in name:
# This is taken directly from OoT. Represents the generally excepted characters.
if (0x20 <= ord(c) < 0x7e):
continue
bad_names.append(name)
break
assert len(bad_names) == 0, f"Muse Dash has {len(bad_names)} songs with non-ASCII characters.\n{bad_names}"

View File

@ -1,7 +1,7 @@
from . import MuseDashTestBase from . import MuseDashTestBase
class TestIncludedSongSizeDoesntGrow(MuseDashTestBase): class TestPlandoSettings(MuseDashTestBase):
options = { options = {
"additional_song_count": 15, "additional_song_count": 15,
"allow_just_as_planned_dlc_songs": True, "allow_just_as_planned_dlc_songs": True,
@ -14,14 +14,14 @@ class TestIncludedSongSizeDoesntGrow(MuseDashTestBase):
def test_included_songs_didnt_grow_item_count(self) -> None: def test_included_songs_didnt_grow_item_count(self) -> None:
muse_dash_world = self.multiworld.worlds[1] muse_dash_world = self.multiworld.worlds[1]
assert len(muse_dash_world.included_songs) == 15, \ self.assertEqual(len(muse_dash_world.included_songs), 15,
f"Logical songs size grew when it shouldn't. Expected 15. Got {len(muse_dash_world.included_songs)}" f"Logical songs size grew when it shouldn't. Expected 15. Got {len(muse_dash_world.included_songs)}")
def test_included_songs_plando(self) -> None: def test_included_songs_plando(self) -> None:
muse_dash_world = self.multiworld.worlds[1] muse_dash_world = self.multiworld.worlds[1]
songs = muse_dash_world.included_songs.copy() songs = muse_dash_world.included_songs.copy()
songs.append(muse_dash_world.victory_song_name) songs.append(muse_dash_world.victory_song_name)
assert "Operation Blade" in songs, "Logical songs is missing a plando song: Operation Blade" self.assertIn("Operation Blade", songs, "Logical songs is missing a plando song: Operation Blade")
assert "Autumn Moods" in songs, "Logical songs is missing a plando song: Autumn Moods" self.assertIn("Autumn Moods", songs, "Logical songs is missing a plando song: Autumn Moods")
assert "Fireflies" in songs, "Logical songs is missing a plando song: Fireflies" self.assertIn("Fireflies", songs, "Logical songs is missing a plando song: Fireflies")

View File

@ -1,25 +0,0 @@
from . import MuseDashTestBase
class TestRemovedSongs(MuseDashTestBase):
options = {
"starting_song_count": 10,
"allow_just_as_planned_dlc_songs": True,
"additional_song_count": 500,
}
removed_songs = [
"CHAOS Glitch",
"FM 17314 SUGAR RADIO"
]
def test_remove_songs_are_not_generated(self) -> None:
# This test is done on a world where every song should be added.
muse_dash_world = self.multiworld.worlds[1]
for song_name in self.removed_songs:
assert song_name not in muse_dash_world.starting_songs, \
f"Song '{song_name}' was included into the starting songs when it shouldn't."
assert song_name not in muse_dash_world.included_songs, \
f"Song '{song_name}' was included into the included songs when it shouldn't."