Compare commits

...

10 Commits

Author SHA1 Message Date
Fabian Dill
77a349c1c6 Core/LttP: remove can_reach_private 2023-08-31 22:10:38 +02:00
agilbert1412
c4a3204af7 Stardew Valley: Add missing special order logic rules (#2136)
* - Added missing special order requirements, mostly for the regions where to place the collected items, or the NPC to talk to when done

* - Added missing requirement on being able to go see the wizard cutscene in order to interact with bundles
2023-08-31 06:45:52 +02:00
Remy Jette
9323f7d892 WebHost: Add a summary row to the Multiworld Tracker (#1965)
* WebHost: Add a summary row to the Multiworld Tracker

Implements suggestions from the generation-suggestions channel:
- https://discord.com/channels/731205301247803413/1124186131911688262
- https://discord.com/channels/731205301247803413/1109513647274856518

* Improve secondsToHours function, and remove jQuery from footerCallback function.

* Don't show the summary row on game-specific multi trackers

---------

Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-08-29 17:58:49 -04:00
Fabian Dill
30e747bb4c Windows: create terminal capable Launcher (#2111) 2023-08-29 20:59:39 +02:00
Justus Lind
9d29c6d301 Muse Dash: Fix bad generations occuring due to changing item ids (#2122) 2023-08-29 20:58:34 +02:00
NewSoupVi
aa19a79d26 Witness: Fix one of the hints not being a Haiku (seriously) (#2123)
I hope this gets a prize for "Most irrelevant PR in AP history"

Explanation:
When changing the hint system on the client side to be able to auto-wrap, decisions were made about which line breaks were still explicitly important, with most of them being removed.

This hint was somewhat devalued in the process.

-. --- - .... .. -. --. translates to "Nothing", which I thought was the entirety of the joke.

However, the line breaks were actually also important, because:

dash dot, dash dash dash,
dash, dot dot dot dot, dot dot,
dash dot, dash dash dot

is a Haiku! And the hint's creator (oddGarrett I believe) said this was specifically part of the creative vision for this joke hint. They said it's fine, I don't need to change it, but I couldn't let that stand.

So, the explicit line breaks for this joke hint are back.
2023-08-29 20:56:40 +02:00
Alchav
5a34471266 Pokémon R/B: Fix fishing logic mistake (#2133) 2023-08-29 20:56:07 +02:00
eudaimonistic
ae96010ff1 [Subnautica] update subnautica/docs/setup_en.md (#2131)
At some point the client-side mod for this world started to include support for the "!" in dev console, rendering this line obsolete.  Updated to reflect current client behavior.
2023-08-29 18:05:12 +02:00
CaitSith2
944fe6cb8c Fix root cause of ALttP hardware crashes on collect. (#2132)
As it turns out, SD2SNES / FXPAK Pro has a limit as to how many bytes can be written in a single command packet.  Exceed this limit, and the hardware will crash.  That limit is 512 bytes.  Even then, I have scaled back to 256 bytes at a time for a margin of safety.
2023-08-29 06:07:31 -07:00
agilbert1412
21baa302d4 [SDV] Added a missing logic rule for talking to Leo (#2129) 2023-08-29 00:55:13 +02:00
22 changed files with 224 additions and 157 deletions

View File

@@ -853,14 +853,6 @@ class Region:
state.update_reachable_regions(self.player) state.update_reachable_regions(self.player)
return self in state.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 @property
def hint_text(self) -> str: def hint_text(self) -> str:
return self._hint_text if self._hint_text else self.name return self._hint_text if self._hint_text else self.name

View File

@@ -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'} PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try: try:
for address, data in write_list: for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]] while data:
# REVIEW: above: `if snes_socket is None: return False` # Divide the write into packets of 256 bytes.
# Does it need to be checked again? PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
if ctx.snes_socket is not None: if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request)) await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data) await ctx.snes_socket.send(data[:256])
else: address += 256
snes_logger.warning(f"Could not send data to SNES: {data}") data = data[256:]
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
except ConnectionClosed: except ConnectionClosed:
return False return False

View File

@@ -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', () => { window.addEventListener('load', () => {
const tables = $(".table").DataTable({ const tables = $(".table").DataTable({
paging: false, paging: false,
@@ -27,7 +38,18 @@ window.addEventListener('load', () => {
stateLoadCallback: function(settings) { stateLoadCallback: function(settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); 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: [ columnDefs: [
{
targets: 'last-activity',
name: 'lastActivity'
},
{ {
targets: 'hours', targets: 'hours',
render: function (data, type, row) { render: function (data, type, row) {
@@ -40,11 +62,7 @@ window.addEventListener('load', () => {
if (data === "None") if (data === "None")
return data; return data;
let hours = Math.floor(data / 3600); return secondsToHours(data);
let minutes = Math.floor((data - (hours * 3600)) / 60);
if (minutes < 10) {minutes = "0"+minutes;}
return hours+':'+minutes;
} }
}, },
{ {
@@ -114,11 +132,16 @@ window.addEventListener('load', () => {
if (status === "success") { if (status === "success") {
target.find(".table").each(function (i, new_table) { target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr"); const new_trs = $(new_table).find("tbody>tr");
const footer_tr = $(new_table).find("tfoot>tr");
const old_table = tables.eq(i); const old_table = tables.eq(i);
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
old_table.clear(); 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).scrollTop(topscroll);
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
}); });

View File

@@ -55,16 +55,16 @@ table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif; font-family: LexendDeca-Regular, sans-serif;
} }
table.dataTable tbody{ table.dataTable tbody, table.dataTable tfoot{
background-color: #dce2bd; background-color: #dce2bd;
font-family: LexendDeca-Light, sans-serif; font-family: LexendDeca-Light, sans-serif;
} }
table.dataTable tbody tr:hover{ table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
background-color: #e2eabb; background-color: #e2eabb;
} }
table.dataTable tbody td{ table.dataTable tbody td, table.dataTable tfoot td{
padding: 4px 6px; padding: 4px 6px;
} }
@@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{
top: 46px; top: 46px;
} }
table.dataTable tbody td{ table.dataTable tbody td, table.dataTable tfoot td{
border: 1px solid #bba967; border: 1px solid #bba967;
} }
table.dataTable tfoot td{
font-weight: bold;
}
div.dataTables_scrollBody{ div.dataTables_scrollBody{
background-color: inherit !important; background-color: inherit !important;
} }

View File

@@ -37,7 +37,7 @@
{% endblock %} {% endblock %}
<th class="center-column">Checks</th> <th class="center-column">Checks</th>
<th class="center-column">&percnt;</th> <th class="center-column">&percnt;</th>
<th class="center-column hours">Last<br>Activity</th> <th class="center-column hours last-activity">Last<br>Activity</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -64,6 +64,19 @@
</tr> </tr>
{%- endfor -%} {%- endfor -%}
</tbody> </tbody>
{% if not self.custom_table_headers() | trim %}
<tfoot>
<tr>
<td></td>
<td>Total</td>
<td>All Games</td>
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
<td class="center-column last-activity"></td>
</tr>
</tfoot>
{% endif %}
</table> </table>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -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 playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)} 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))} hints = {team: set() for team in range(len(names))}
if room.multisave: if room.multisave:
multisave = restricted_loads(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) activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {} player_names = {}
completed_worlds = 0
states: typing.Dict[typing.Tuple[int, int], int] = {} states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names): for team, names in enumerate(names):
for player, name in enumerate(names, 1): for player, name in enumerate(names, 1):
player_names[team, player] = name player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0) 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() long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items(): for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias 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, activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names, long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups, 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, custom_locations=custom_locations, custom_items=custom_items,
) )

View File

@@ -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}\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.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}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio

View File

@@ -185,13 +185,22 @@ def resolve_icon(icon_name: str):
exes = [ exes = [
cx_Freeze.Executable( 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 ""), target_name=c.frozen_name + (".exe" if is_windows else ""),
icon=resolve_icon(c.icon), icon=resolve_icon(c.icon),
base="Win32GUI" if is_windows and not c.cli else None base="Win32GUI" if is_windows and not c.cli else None
) for c in components if c.script_name and c.frozen_name ) 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_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []

View File

@@ -32,7 +32,6 @@ def set_rules(world):
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
if world.players == 1: if world.players == 1:
world.get_region('Menu', player).can_reach_private = lambda state: True
no_logic_rules(world, player) no_logic_rules(world, player)
for exit in world.get_region('Menu', player).exits: for exit in world.get_region('Menu', player).exits:
exit.hide_path = True exit.hide_path = True
@@ -196,7 +195,6 @@ def global_rules(world, player):
add_item_rule(world.get_location(prize_location, player), add_item_rule(world.get_location(prize_location, player),
lambda item: item.name in crystals_and_pendants and item.player == 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 # 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: for exit in world.get_region('Menu', player).exits:
exit.hide_path = True exit.hide_path = True

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

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

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, "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", "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, "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, "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 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)) 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({ self.special_order_rules.update({
SpecialOrder.island_ingredients: self.has_island_transport() & self.can_farm_perfectly() & SpecialOrder.island_ingredients: self.can_meet(NPC.caroline) & self.has_island_transport() & self.can_farm_perfectly() &
self.has(Vegetable.taro_root) & self.has(Fruit.pineapple) & self.has(Forageable.ginger), self.can_ship(Vegetable.taro_root) & self.can_ship(Fruit.pineapple) & self.can_ship(Forageable.ginger),
SpecialOrder.cave_patrol: self.can_mine_perfectly() & self.can_mine_to_floor(120), SpecialOrder.cave_patrol: self.can_meet(NPC.clint) & self.can_mine_perfectly() & self.can_mine_to_floor(120),
SpecialOrder.aquatic_overpopulation: self.can_fish_perfectly(), SpecialOrder.aquatic_overpopulation: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
SpecialOrder.biome_balance: self.can_fish_perfectly(), SpecialOrder.biome_balance: self.can_meet(NPC.demetrius) & self.can_fish_perfectly(),
SpecialOrder.rock_rejuivenation: self.has(Mineral.ruby) & self.has(Mineral.topaz) & self.has(Mineral.emerald) & SpecialOrder.rock_rejuivenation: self.has_relationship(NPC.emily, 4) & self.has(Mineral.ruby) & self.has(Mineral.topaz) &
self.has(Mineral.jade) & self.has(Mineral.amethyst) & self.has_relationship(NPC.emily, 4) & self.has(Mineral.emerald) & self.has(Mineral.jade) & self.has(Mineral.amethyst) &
self.has(ArtisanGood.cloth) & self.can_reach_region(Region.haley_house), 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.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.dig_site), 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.has(AnimalProduct.any_egg), SpecialOrder.gus_famous_omelet: self.can_reach_region(Region.saloon) & self.has(AnimalProduct.any_egg),
SpecialOrder.crop_order: self.can_farm_perfectly(), SpecialOrder.crop_order: self.can_farm_perfectly() & self.can_ship(),
SpecialOrder.community_cleanup: self.can_crab_pot(), SpecialOrder.community_cleanup: self.can_reach_region(Region.railroad) & self.can_crab_pot(),
SpecialOrder.the_strong_stuff: self.can_keg(Vegetable.potato), SpecialOrder.the_strong_stuff: self.can_reach_region(Region.trailer) & self.can_keg(Vegetable.potato),
SpecialOrder.pierres_prime_produce: self.can_farm_perfectly(), SpecialOrder.pierres_prime_produce: self.can_reach_region(Region.pierre_store) & self.can_farm_perfectly(),
SpecialOrder.robins_project: self.can_chop_perfectly() & self.has(Material.hardwood), SpecialOrder.robins_project: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
SpecialOrder.robins_resource_rush: self.can_chop_perfectly() & self.has(Fertilizer.tree) & self.can_mine_perfectly(), self.has(Material.hardwood),
SpecialOrder.juicy_bugs_wanted_yum: self.has(Loot.bug_meat), SpecialOrder.robins_resource_rush: self.can_meet(NPC.robin) & self.can_reach_region(Region.carpenter) & self.can_chop_perfectly() &
SpecialOrder.tropical_fish: self.has_island_transport() & self.has(Fish.stingray) & self.has(Fish.blue_discus) & self.has(Fish.lionfish), self.has(Fertilizer.tree) & self.can_mine_perfectly(),
SpecialOrder.a_curious_substance: self.can_mine_perfectly() & self.can_mine_to_floor(80), SpecialOrder.juicy_bugs_wanted_yum: self.can_reach_region(Region.beach) & self.has(Loot.bug_meat),
SpecialOrder.prismatic_jelly: self.can_mine_perfectly() & self.can_mine_to_floor(40), 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) & 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.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.lets_play_a_game: self.has_junimo_kart_max_level(),
SpecialOrder.four_precious_stones: self.has_lived_months(MAX_MONTHS) & self.has("Prismatic Shard") & SpecialOrder.four_precious_stones: self.has_lived_months(MAX_MONTHS) & self.has("Prismatic Shard") &
self.can_mine_perfectly_in_the_skull_cavern(), 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_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.qis_kindness: self.can_give_loved_gifts_to_everyone(),
SpecialOrder.extended_family: self.can_fish_perfectly() & self.has(Fish.angler) & self.has(Fish.glacierfish) & 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), 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)] rules = [self.can_reach_any_region(villager.locations)]
if npc == NPC.kent: if npc == NPC.kent:
rules.append(self.has_year_two()) rules.append(self.has_year_two())
elif npc == NPC.leo:
rules.append(self.received("Island West Turtle"))
return And(rules) return And(rules)
@@ -1155,7 +1161,7 @@ class StardewLogic:
item_rules.append(bundle_item.item.name) item_rules.append(bundle_item.item.name)
if bundle_item.quality > highest_quality_yet: if bundle_item.quality > highest_quality_yet:
highest_quality_yet = bundle_item.quality 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: def can_grow_gold_quality(self, quality: int) -> StardewRule:
if quality <= 0: if quality <= 0:
@@ -1603,3 +1609,9 @@ class StardewLogic:
rules.append(self.received(f"Rarecrow #{rarecrow_number}")) rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
return And(rules) 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: The mod adds the following console commands:
- `say` sends the text following it to Archipelago as a chat message. - `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. - `silent` toggles Archipelago messages appearing.
- `tracker` rotates through the possible settings for the in-game tracker that displays the closest uncollected location. - `tracker` rotates through the possible settings for the in-game tracker that displays the closest uncollected location.
- `deathlink` toggles death link. - `deathlink` toggles death link.

View File

@@ -10,7 +10,7 @@ joke_hints = [
"You can do it!", "You can do it!",
"I believe in you!", "I believe in you!",
"The person playing is cute. <3", "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.", "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", "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.", "Thanks to the Archipelago developers for making this possible.",