diff --git a/BaseClasses.py b/BaseClasses.py index 02d050c667..26cdfb5285 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -853,14 +853,6 @@ class Region: state.update_reachable_regions(self.player) return self in state.reachable_regions[self.player] - def can_reach_private(self, state: CollectionState) -> bool: - for entrance in self.entrances: - if entrance.can_reach(state): - if not self in state.path: - state.path[self] = (self.name, state.path.get(entrance, None)) - return True - return False - @property def hint_text(self) -> str: return self._hint_text if self._hint_text else self.name diff --git a/SNIClient.py b/SNIClient.py index 50b557e6d7..66d0b2ca9c 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -565,14 +565,16 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int, PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} try: for address, data in write_list: - PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] - # REVIEW: above: `if snes_socket is None: return False` - # Does it need to be checked again? - if ctx.snes_socket is not None: - await ctx.snes_socket.send(dumps(PutAddress_Request)) - await ctx.snes_socket.send(data) - else: - snes_logger.warning(f"Could not send data to SNES: {data}") + while data: + # Divide the write into packets of 256 bytes. + PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]] + if ctx.snes_socket is not None: + await ctx.snes_socket.send(dumps(PutAddress_Request)) + await ctx.snes_socket.send(data[:256]) + address += 256 + data = data[256:] + else: + snes_logger.warning(f"Could not send data to SNES: {data}") except ConnectionClosed: return False diff --git a/WebHostLib/static/assets/trackerCommon.js b/WebHostLib/static/assets/trackerCommon.js index c08590cbf7..41c4020dac 100644 --- a/WebHostLib/static/assets/trackerCommon.js +++ b/WebHostLib/static/assets/trackerCommon.js @@ -14,6 +14,17 @@ const adjustTableHeight = () => { } }; +/** + * Convert an integer number of seconds into a human readable HH:MM format + * @param {Number} seconds + * @returns {string} + */ +const secondsToHours = (seconds) => { + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0'); + return `${hours}:${minutes}`; +}; + window.addEventListener('load', () => { const tables = $(".table").DataTable({ paging: false, @@ -27,7 +38,18 @@ window.addEventListener('load', () => { stateLoadCallback: function(settings) { return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); }, + footerCallback: function(tfoot, data, start, end, display) { + if (tfoot) { + const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x)); + Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText = + (activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None'; + } + }, columnDefs: [ + { + targets: 'last-activity', + name: 'lastActivity' + }, { targets: 'hours', render: function (data, type, row) { @@ -40,11 +62,7 @@ window.addEventListener('load', () => { if (data === "None") return data; - let hours = Math.floor(data / 3600); - let minutes = Math.floor((data - (hours * 3600)) / 60); - - if (minutes < 10) {minutes = "0"+minutes;} - return hours+':'+minutes; + return secondsToHours(data); } }, { @@ -114,11 +132,16 @@ window.addEventListener('load', () => { if (status === "success") { target.find(".table").each(function (i, new_table) { const new_trs = $(new_table).find("tbody>tr"); + const footer_tr = $(new_table).find("tfoot>tr"); const old_table = tables.eq(i); const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); old_table.clear(); - old_table.rows.add(new_trs).draw(); + if (footer_tr.length) { + $(old_table.table).find("tfoot").html(footer_tr); + } + old_table.rows.add(new_trs); + old_table.draw(); $(old_table.settings()[0].nScrollBody).scrollTop(topscroll); $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); }); diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index 0e00553c72..0cc2ede59f 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -55,16 +55,16 @@ table.dataTable thead{ font-family: LexendDeca-Regular, sans-serif; } -table.dataTable tbody{ +table.dataTable tbody, table.dataTable tfoot{ background-color: #dce2bd; font-family: LexendDeca-Light, sans-serif; } -table.dataTable tbody tr:hover{ +table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{ background-color: #e2eabb; } -table.dataTable tbody td{ +table.dataTable tbody td, table.dataTable tfoot td{ padding: 4px 6px; } @@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{ top: 46px; } -table.dataTable tbody td{ +table.dataTable tbody td, table.dataTable tfoot td{ border: 1px solid #bba967; } +table.dataTable tfoot td{ + font-weight: bold; +} + div.dataTables_scrollBody{ background-color: inherit !important; } diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 2232cd0fd1..40d89eb4c6 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -37,7 +37,7 @@ {% endblock %} Checks % - Last
Activity + Last
Activity @@ -64,6 +64,19 @@ {%- endfor -%} + {% if not self.custom_table_headers() | trim %} + + + + Total + All Games + {{ completed_worlds }}/{{ players|length }} Complete + {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} + {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} + + + + {% endif %} {% endfor %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index d3fd0fb036..80ccc720a3 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1366,6 +1366,10 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} + total_locations = {teamnumber: sum(len(locations[playernumber]) + for playernumber in range(1, len(team) + 1) if playernumber not in groups) + for teamnumber, team in enumerate(names)} + hints = {team: set() for team in range(len(names))} if room.multisave: multisave = restricted_loads(room.multisave) @@ -1390,11 +1394,14 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) player_names = {} + completed_worlds = 0 states: typing.Dict[typing.Tuple[int, int], int] = {} for team, names in enumerate(names): for player, name in enumerate(names, 1): player_names[team, player] = name states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) + if states[team, player] == 30: # Goal Completed + completed_worlds += 1 long_player_names = player_names.copy() for (team, player), alias in multisave.get("name_aliases", {}).items(): player_names[team, player] = alias @@ -1410,7 +1417,8 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s activity_timers=activity_timers, video=video, hints=hints, long_player_names=long_player_names, multisave=multisave, precollected_items=precollected_items, groups=groups, - locations=locations, games=games, states=states, + locations=locations, total_locations=total_locations, games=games, states=states, + completed_worlds=completed_worlds, custom_locations=custom_locations, custom_items=custom_items, ) diff --git a/inno_setup.iss b/inno_setup.iss index d87ae857ea..147cd74dca 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -116,6 +116,7 @@ Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; +Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion; Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio diff --git a/setup.py b/setup.py index b5b1af3a32..212bcc5d09 100644 --- a/setup.py +++ b/setup.py @@ -185,13 +185,22 @@ def resolve_icon(icon_name: str): exes = [ cx_Freeze.Executable( - script=f'{c.script_name}.py', + script=f"{c.script_name}.py", target_name=c.frozen_name + (".exe" if is_windows else ""), icon=resolve_icon(c.icon), base="Win32GUI" if is_windows and not c.cli else None ) for c in components if c.script_name and c.frozen_name ] +if is_windows: + # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help + c = next(component for component in components if component.script_name == "Launcher") + exes.append(cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name=f"{c.frozen_name}(DEBUG).exe", + icon=resolve_icon(c.icon), + )) + extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 09c63aca01..ce4a941ead 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -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 diff --git a/worlds/musedash/Items.py b/worlds/musedash/Items.py index 84dd1c555d..be229228bd 100644 --- a/worlds/musedash/Items.py +++ b/worlds/musedash/Items.py @@ -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): diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 66753935c7..7812e28b7a 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -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]: diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index a7a5e67e7b..8d6c3f3753 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -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 \ No newline at end of file +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| \ No newline at end of file diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index f34142a20b..78b9c253d5 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -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() diff --git a/worlds/musedash/test/TestCollection.py b/worlds/musedash/test/TestCollection.py new file mode 100644 index 0000000000..23348af104 --- /dev/null +++ b/worlds/musedash/test/TestCollection.py @@ -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.") diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py index f43b677935..58817d0fc3 100644 --- a/worlds/musedash/test/TestDifficultyRanges.py +++ b/worlds/musedash/test/TestDifficultyRanges.py @@ -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.") diff --git a/worlds/musedash/test/TestNames.py b/worlds/musedash/test/TestNames.py deleted file mode 100644 index 0629afc62a..0000000000 --- a/worlds/musedash/test/TestNames.py +++ /dev/null @@ -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}" diff --git a/worlds/musedash/test/TestPlandoSettings.py b/worlds/musedash/test/TestPlandoSettings.py index a6bd8ad273..4b23a4afa9 100644 --- a/worlds/musedash/test/TestPlandoSettings.py +++ b/worlds/musedash/test/TestPlandoSettings.py @@ -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" \ No newline at end of file + 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") \ No newline at end of file diff --git a/worlds/musedash/test/TestRemovedSongs.py b/worlds/musedash/test/TestRemovedSongs.py deleted file mode 100644 index 838c64b5dc..0000000000 --- a/worlds/musedash/test/TestRemovedSongs.py +++ /dev/null @@ -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." diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index d82e5bb953..cc788dd2ba 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -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)) diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 3d8209710e..46580d549a 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -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) + diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index e653f53145..83f4186bdf 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -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. diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index d108e8bfe4..5d8bd5d370 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -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.",