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.",