Merge branch 'ArchipelagoMW:main' into main

This commit is contained in:
CookieCat
2023-09-02 10:54:50 -04:00
committed by GitHub
22 changed files with 224 additions and 157 deletions

View File

@@ -32,7 +32,6 @@ def set_rules(world):
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
if world.players == 1:
world.get_region('Menu', player).can_reach_private = lambda state: True
no_logic_rules(world, player)
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
@@ -196,7 +195,6 @@ def global_rules(world, player):
add_item_rule(world.get_location(prize_location, player),
lambda item: item.name in crystals_and_pendants and item.player == player)
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
world.get_region('Menu', player).can_reach_private = lambda state: True
for exit in world.get_region('Menu', player).exits:
exit.hide_path = True

View File

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

View File

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

View File

@@ -464,4 +464,8 @@ Songs Are Judged 90% by Chorus feat. Mameko|64-3|COSMIC RADIO PEROLIST|True|6|8|
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|
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"
option_definitions = musedash_options
topology_present = False
data_version = 8
data_version = 9
web = MuseDashWebWorld()
# Necessary Data
md_collection = MuseDashCollections(2900000, 2)
md_collection = MuseDashCollections()
item_name_to_id = md_collection.item_names_to_id
location_name_to_id = md_collection.location_names_to_id
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
# Working Data
victory_song_name: str = ""
@@ -167,11 +167,12 @@ class MuseDashWorld(World):
if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
song = self.md_collection.song_items.get(name)
if song:
return MuseDashSongItem(name, self.player, song)
album = self.md_collection.album_items.get(name)
if album:
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:
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]
def test_range(inputRange, lower, upper):
assert inputRange[0] == lower and inputRange[1] == upper, \
f"Output incorrect. Got: {inputRange[0]} to {inputRange[1]}. Expected: {lower} to {upper}"
self.assertEqual(inputRange[0], lower)
self.assertEqual(inputRange[1], upper)
songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1])
for songKey in songs:
@@ -24,7 +24,7 @@ class DifficultyRanges(MuseDashTestBase):
if (song.master is not None and inputRange[0] <= song.master <= inputRange[1]):
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
difficulty_choice.value = 0
@@ -65,5 +65,5 @@ class DifficultyRanges(MuseDashTestBase):
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
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, \
f"Song '{song_name}' difficulty not set when it should be."
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.")

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
class TestIncludedSongSizeDoesntGrow(MuseDashTestBase):
class TestPlandoSettings(MuseDashTestBase):
options = {
"additional_song_count": 15,
"allow_just_as_planned_dlc_songs": True,
@@ -14,14 +14,14 @@ class TestIncludedSongSizeDoesntGrow(MuseDashTestBase):
def test_included_songs_didnt_grow_item_count(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
assert 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)}"
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)}")
def test_included_songs_plando(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
songs = muse_dash_world.included_songs.copy()
songs.append(muse_dash_world.victory_song_name)
assert "Operation Blade" in songs, "Logical songs is missing a plando song: Operation Blade"
assert "Autumn Moods" in songs, "Logical songs is missing a plando song: Autumn Moods"
assert "Fireflies" in songs, "Logical songs is missing a plando song: Fireflies"
self.assertIn("Operation Blade", songs, "Logical songs is missing a plando song: Operation Blade")
self.assertIn("Autumn Moods", songs, "Logical songs is missing a plando song: Autumn Moods")
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."

View File

@@ -1681,7 +1681,6 @@ def create_regions(self):
connect(multiworld, player, "Fuchsia City", "Fuchsia Fishing", lambda state: state.has("Super Rod", player), one_way=True)
connect(multiworld, player, "Pallet Town", "Old Rod Fishing", lambda state: state.has("Old Rod", player), one_way=True)
connect(multiworld, player, "Pallet Town", "Good Rod Fishing", lambda state: state.has("Good Rod", player), one_way=True)
connect(multiworld, player, "Cinnabar Lab Fossil Room", "Good Rod Fishing", one_way=True)
connect(multiworld, player, "Cinnabar Lab Fossil Room", "Fossil Level", lambda state: logic.fossil_checks(state, 1, player), one_way=True)
connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, player))
connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, player))

View File

@@ -492,35 +492,39 @@ class StardewLogic:
})
self.special_order_rules.update({
SpecialOrder.island_ingredients: self.has_island_transport() & self.can_farm_perfectly() &
self.has(Vegetable.taro_root) & self.has(Fruit.pineapple) & self.has(Forageable.ginger),
SpecialOrder.cave_patrol: self.can_mine_perfectly() & self.can_mine_to_floor(120),
SpecialOrder.aquatic_overpopulation: self.can_fish_perfectly(),
SpecialOrder.biome_balance: self.can_fish_perfectly(),
SpecialOrder.rock_rejuivenation: self.has(Mineral.ruby) & self.has(Mineral.topaz) & self.has(Mineral.emerald) &
self.has(Mineral.jade) & self.has(Mineral.amethyst) & self.has_relationship(NPC.emily, 4) &
self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house),
SpecialOrder.gifts_for_george: self.has_season(Season.spring) & self.has(Forageable.leek),
SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.dig_site),
SpecialOrder.gus_famous_omelet: self.has(AnimalProduct.any_egg),
SpecialOrder.crop_order: self.can_farm_perfectly(),
SpecialOrder.community_cleanup: self.can_crab_pot(),
SpecialOrder.the_strong_stuff: self.can_keg(Vegetable.potato),
SpecialOrder.pierres_prime_produce: self.can_farm_perfectly(),
SpecialOrder.robins_project: self.can_chop_perfectly() & self.has(Material.hardwood),
SpecialOrder.robins_resource_rush: self.can_chop_perfectly() & self.has(Fertilizer.tree) & self.can_mine_perfectly(),
SpecialOrder.juicy_bugs_wanted_yum: self.has(Loot.bug_meat),
SpecialOrder.tropical_fish: self.has_island_transport() & self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish),
SpecialOrder.a_curious_substance: self.can_mine_perfectly() & self.can_mine_to_floor(80),
SpecialOrder.prismatic_jelly: self.can_mine_perfectly() & self.can_mine_to_floor(40),
SpecialOrder.island_ingredients: self.can_meet(NPC.caroline) & self.has_island_transport() & self.can_farm_perfectly() &
self.can_ship(Vegetable.taro_root) & self.can_ship(Fruit.pineapple) & self.can_ship(Forageable.ginger),
SpecialOrder.cave_patrol: self.can_meet(NPC.clint) & self.can_mine_perfectly() & self.can_mine_to_floor(120),
SpecialOrder.aquatic_overpopulation: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
SpecialOrder.biome_balance: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
SpecialOrder.rock_rejuivenation: self.has_relationship(NPC.emily, 4) & self.has(Mineral.ruby) & self.has(Mineral.topaz) &
self.has(Mineral.emerald) & self.has(Mineral.jade) & self.has(Mineral.amethyst) &
self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house),
SpecialOrder.gifts_for_george: self.can_reach_region(Region.alex_house) & self.has_season(Season.spring) & self.has(Forageable.leek),
SpecialOrder.fragments_of_the_past: self.can_reach_region(Region.museum) & self.can_reach_region(Region.dig_site) & self.has_tool(Tool.pickaxe),
SpecialOrder.gus_famous_omelet: self.can_reach_region(Region.saloon) & self.has(AnimalProduct.any_egg),
SpecialOrder.crop_order: self.can_farm_perfectly() & self.can_ship(),
SpecialOrder.community_cleanup: self.can_reach_region(Region.railroad) & self.can_crab_pot(),
SpecialOrder.the_strong_stuff: self.can_reach_region(Region.trailer) & self.can_keg(Vegetable.potato),
SpecialOrder.pierres_prime_produce: self.can_reach_region(Region.pierre_store) & self.can_farm_perfectly(),
SpecialOrder.robins_project: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
self.has(Material.hardwood),
SpecialOrder.robins_resource_rush: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
self.has(Fertilizer.tree) & self.can_mine_perfectly(),
SpecialOrder.juicy_bugs_wanted_yum: self.can_reach_region(Region.beach) & self.has(Loot.bug_meat),
SpecialOrder.tropical_fish: self.can_meet(NPC.willy) & self.received("Island Resort") & self.has_island_transport() &
self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish),
SpecialOrder.a_curious_substance: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(80),
SpecialOrder.prismatic_jelly: self.can_reach_region(Region.wizard_tower) & self.can_mine_perfectly() & self.can_mine_to_floor(40),
SpecialOrder.qis_crop: self.can_farm_perfectly() & self.can_reach_region(Region.greenhouse) &
self.can_reach_region(Region.island_west) & self.has_total_skill_level(50) &
self.has(Machine.seed_maker),
self.has(Machine.seed_maker) & self.has_building(Building.shipping_bin),
SpecialOrder.lets_play_a_game: self.has_junimo_kart_max_level(),
SpecialOrder.four_precious_stones: self.has_lived_months(MAX_MONTHS) & self.has("Prismatic Shard") &
self.can_mine_perfectly_in_the_skull_cavern(),
SpecialOrder.qis_hungry_challenge: self.can_mine_perfectly_in_the_skull_cavern() & self.has_max_buffs(),
SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)),
SpecialOrder.qis_cuisine: self.can_cook() & (self.can_spend_money_at(Region.saloon, 205000) | self.can_spend_money_at(Region.pierre_store, 170000)) &
self.can_ship(),
SpecialOrder.qis_kindness: self.can_give_loved_gifts_to_everyone(),
SpecialOrder.extended_family: self.can_fish_perfectly() & self.has(Fish.angler) & self.has(Fish.glacierfish) &
self.has(Fish.crimsonfish) & self.has(Fish.mutant_carp) & self.has(Fish.legend),
@@ -1095,6 +1099,8 @@ class StardewLogic:
rules = [self.can_reach_any_region(villager.locations)]
if npc == NPC.kent:
rules.append(self.has_year_two())
elif npc == NPC.leo:
rules.append(self.received("Island West Turtle"))
return And(rules)
@@ -1155,7 +1161,7 @@ class StardewLogic:
item_rules.append(bundle_item.item.name)
if bundle_item.quality > highest_quality_yet:
highest_quality_yet = bundle_item.quality
return self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet)
return self.can_reach_region(Region.wizard_tower) & self.has(item_rules, number_required) & self.can_grow_gold_quality(highest_quality_yet)
def can_grow_gold_quality(self, quality: int) -> StardewRule:
if quality <= 0:
@@ -1603,3 +1609,9 @@ class StardewLogic:
rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
return And(rules)
def can_ship(self, item: str = "") -> StardewRule:
shipping_bin_rule = self.has_building(Building.shipping_bin)
if item == "":
return shipping_bin_rule
return shipping_bin_rule & self.has(item)

View File

@@ -36,7 +36,7 @@ Warning: Currently it is not checked whether a loaded savegame belongs to the mu
The mod adds the following console commands:
- `say` sends the text following it to Archipelago as a chat message.
- `!` is not an allowed character, use `/` in its place. For example, to use the [`!hint` command](/tutorial/Archipelago/commands/en#remote-commands), type `say /hint`.
- For example, to use the [`!hint` command](/tutorial/Archipelago/commands/en#remote-commands), type `say !hint`.
- `silent` toggles Archipelago messages appearing.
- `tracker` rotates through the possible settings for the in-game tracker that displays the closest uncollected location.
- `deathlink` toggles death link.

View File

@@ -10,7 +10,7 @@ joke_hints = [
"You can do it!",
"I believe in you!",
"The person playing is cute. <3",
"dash dot, dash dash dash, dash, dot dot dot dot, dot dot, dash dot, dash dash dot",
"dash dot, dash dash dash,\ndash, dot dot dot dot, dot dot,\ndash dot, dash dash dot",
"When you think about it, there are actually a lot of bubbles in a stream.",
"Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you",
"Thanks to the Archipelago developers for making this possible.",