diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f524fbdf11a..c5a616aaab62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865" CMAKE_BIN="$ANDROID_HOME/cmake/3.22.1/bin/cmake" - for ABI in arm64-v8a armeabi-v7a; do + for ABI in arm64-v8a armeabi-v7a x86; do echo "::group::Building osu_native for $ABI" "$CMAKE_BIN" -B "build-native/$ABI" -S osu.Android/Native \ -DCMAKE_TOOLCHAIN_FILE="$NDK_HOME/build/cmake/android.toolchain.cmake" \ @@ -91,46 +91,49 @@ jobs: - name: Build Android APK (signed) if: steps.keystore.outputs.has_keystore == 'true' - run: > - dotnet publish -c Release - osu.Android/osu.Android.csproj - -f net10.0-android - -p:Version=${{ steps.version.outputs.version }} - -p:ApplicationDisplayVersion=${{ steps.version.outputs.version }} - -p:ApplicationVersion=${{ github.run_number }} - -p:AndroidKeyStore=true - -p:AndroidSigningKeyStore=${{ github.workspace }}/osu.Android/osu.keystore - -p:AndroidSigningKeyAlias=${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} - -p:AndroidSigningKeyPass=${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }} - -p:AndroidSigningStorePass=${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }} + run: | + dotnet publish -c Release \ + osu.Android/osu.Android.csproj \ + -f net10.0-android \ + -p:Version="${{ steps.version.outputs.version }}" \ + -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" \ + -p:ApplicationVersion="${{ github.run_number }}" \ + -p:AndroidKeyStore=true \ + -p:AndroidSigningKeyStore="${{ github.workspace }}/osu.Android/osu.keystore" \ + -p:AndroidSigningKeyAlias="${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}" \ + -p:AndroidSigningKeyPass="${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}" \ + -p:AndroidSigningStorePass="${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}" - name: Build Android APK (unsigned) if: steps.keystore.outputs.has_keystore != 'true' - run: > - dotnet publish -c Release - osu.Android/osu.Android.csproj - -f net10.0-android - -p:Version=${{ steps.version.outputs.version }} - -p:ApplicationDisplayVersion=${{ steps.version.outputs.version }} - -p:ApplicationVersion=${{ github.run_number }} - -p:AndroidKeyStore=false + run: | + dotnet publish -c Release \ + osu.Android/osu.Android.csproj \ + -f net10.0-android \ + -p:Version="${{ steps.version.outputs.version }}" \ + -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" \ + -p:ApplicationVersion="${{ github.run_number }}" \ + -p:AndroidKeyStore=false - name: Find APK id: find_apk run: | - # The signed/final APK is inside the publish/ directory in net10.0-android + # Strictly prioritize the publish/ directory where the signed APK resides PUBLISH_DIR="osu.Android/bin/Release/net10.0-android/publish" + echo "Searching for APK in: $PUBLISH_DIR" APK=$(find "$PUBLISH_DIR" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1) + if [ -z "$APK" ]; then - # Fallback to the parent directory if publish/ doesn't exist for some reason - PUBLISH_DIR="osu.Android/bin/Release/net10.0-android" - APK=$(find "$PUBLISH_DIR" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1) + echo "::warning::Signed APK not found in publish directory. Falling back to bin/Release (MAY BE UNSIGNED!)" + APK=$(find osu.Android/bin/Release -name "*.apk" | head -1) fi + if [ -z "$APK" ]; then - # Last resort: recursive find - APK=$(find osu.Android/bin/Release -name "*.apk" | head -1) + echo "::error::No APK found anywhere in bin/Release!" + sh -c "false" fi - echo "Found APK at: $APK" + + echo "Final APK found at: $APK" echo "apk_path=$APK" >> "$GITHUB_OUTPUT" - name: Upload APK artifact diff --git a/build/PatchElfPageSize.targets b/build/PatchElfPageSize.targets index f29ae542eb77..9be15f4da475 100644 --- a/build/PatchElfPageSize.targets +++ b/build/PatchElfPageSize.targets @@ -265,11 +265,10 @@ finally Patches any 64-bit .so in the NuGet cache that has sub-16 KB LOAD alignment. The patch is idempotent — already-aligned files are skipped. --> - + - <_NativeLibs Include="$(NuGetPackageRoot)/**/*.so" /> - <_NativeLibs Include="$(MSBuildThisFileDirectory)../osu.Android/libs/**/*.so" /> + <_NuGetNativeLibs Include="$(NuGetPackageRoot)/**/runtimes/android-*/native/*.so" /> - + diff --git a/debug_ids.py b/debug_ids.py deleted file mode 100644 index 091a478b3c0a..000000000000 --- a/debug_ids.py +++ /dev/null @@ -1,9 +0,0 @@ -import os - -test_path = 'osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs' -with open(test_path, 'r') as f: - lines = f.readlines() - -for i, line in enumerate(lines): - if 'assertItemInQueueListStep' in line or 'addItemStep' in line: - print(f"{i+1}: {line.strip()}") diff --git a/final_cleanup.py b/final_cleanup.py deleted file mode 100644 index 21e8e107dfa9..000000000000 --- a/final_cleanup.py +++ /dev/null @@ -1,28 +0,0 @@ -import re - -def fix_file(path, pattern, replacement): - with open(path, 'r') as f: - content = f.read() - new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE | re.DOTALL) - with open(path, 'w') as f: - f.write(new_content) - -# 1. Fix TestMultiplayerClient spacing and duplicates -fix_file('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', - r'\s+private T clone\(T incoming\).*?return result;\s+\}', - '\n\n private T clone(T incoming)\n {\n byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);\n var result = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS);\n\n if (incoming is MultiplayerRoomUser sourceUser && result is MultiplayerRoomUser targetUser)\n targetUser.User = sourceUser.User;\n\n if (incoming is MultiplayerRoom sourceRoom && result is MultiplayerRoom targetRoom)\n {\n foreach (var user in targetRoom.Users)\n user.User = sourceRoom.Users.FirstOrDefault(u => u.UserID == user.UserID)?.User;\n\n if (targetRoom.Host != null)\n targetRoom.Host.User = sourceRoom.Host?.User;\n }\n else if (incoming is MultiplayerRoomUser sourceSingleUser && result is MultiplayerRoomUser targetSingleUser)\n {\n targetSingleUser.User = sourceSingleUser.User;\n }\n\n return result;\n }') - -# 2. Fix updatePlaylistOrder indentation -fix_file('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', - r'orderedActiveItems = itemsByPriority\s+\.OrderBy', - 'orderedActiveItems = itemsByPriority\n .OrderBy') - -# 3. Fix GameplayWarmupScreen unnecessary using -fix_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', - r'using osu\.Framework\.Logging;\s+', - '') - -# 4. Fix PlayerPanelOverlay null check simplification -fix_file('osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs', - r'if \(panels\.FirstOrDefault\(p => p\.RoomUser\.Equals\(user\)\) is PlayerPanel panel\) panel\.HasQuit = true;', - 'var panel = panels.FirstOrDefault(p => p.RoomUser.Equals(user));\n if (panel != null) panel.HasQuit = true;') diff --git a/final_cleanup_v2.py b/final_cleanup_v2.py deleted file mode 100644 index f7fc478a6245..000000000000 --- a/final_cleanup_v2.py +++ /dev/null @@ -1,66 +0,0 @@ -import re - -def fix_file(path, pattern, replacement): - with open(path, 'r') as f: - content = f.read() - new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE | re.DOTALL) - if new_content == content: - print(f"Warning: No change to {path}") - with open(path, 'w') as f: - f.write(new_content) - -# DailyChallenge.cs cleanup -# presentScore -fix_file('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', - r'private void presentScore\(long id\).*?\{.*?if \(this\.IsCurrentScreen\(\) && playlistItem != null\).*?this\.Push\(new PlaylistItemScoreResultsScreen\(id, room\.RoomID \?\? 0, playlistItem\)\);.*?\}', - ''' private void presentScore(long id) - { - if (this.IsCurrentScreen() && playlistItem != null) - this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID ?? 0, playlistItem)); - }''') - -# updateMods -fix_file('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', - r'private void updateMods\(\).*?\{.*?if \(!this\.IsCurrentScreen\(\) \|\| playlistItem == null\).*?return;.*?Mods\.Value = userMods\.Value\.Concat\(playlistItem\.RequiredMods\.Select\(m => m\.ToMod\(Ruleset\.Value\.CreateInstance\(\)\)\)\)\.ToList\(\);.*?\}', - ''' private void updateMods() - { - if (!this.IsCurrentScreen() || playlistItem == null) - return; - - Mods.Value = userMods.Value.Concat(playlistItem.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); - }''') - -# startPlay -fix_file('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', - r'private void startPlay\(\).*?\{.*?sampleStart\?\.Play\(\);.*?var item = playlistItem;.*?if \(item == null\) return;.*?this\.Push\(new PlayerLoader\(\(\) => new DailyChallengePlayer\(room, item\).*?\{.*?Exited = \(\) => Scheduler\.AddOnce\(\(\) => leaderboard\.RefetchScores\(\)\).*?\}\)\);.*?\}', - ''' private void startPlay() - { - sampleStart?.Play(); - - if (playlistItem == null) - return; - - this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem) - { - Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) - })); - }''') - -# PresentBeatmap -fix_file('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', - r'public void PresentBeatmap\(WorkingBeatmap beatmap, RulesetInfo ruleset\).*?\{.*?if \(!this\.IsCurrentScreen\(\)\).*?return;.*?var item = playlistItem;.*?if \(item == null\) return;.*?if \(item\.Beatmap\.BeatmapSet != null && beatmap\.BeatmapSetInfo\.OnlineID != item\.Beatmap\.BeatmapSet\.OnlineID\).*?\{.*?this\.Exit\(\);.*?game\?\.PresentBeatmap\(beatmap\.BeatmapSetInfo, b => b\.ID == beatmap\.BeatmapInfo\.ID\);.*?\}.*?\}', - ''' public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) - { - if (!this.IsCurrentScreen() || playlistItem == null) - return; - - // We can only handle the current daily challenge beatmap. - // If the import was for a different beatmap, pass the duty off to global handling. - if (playlistItem.Beatmap.BeatmapSet != null && beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet.OnlineID) - { - this.Exit(); - game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); - } - - // And if we're handling, we don't really have much to do here. - }''') diff --git a/final_fix.py b/final_fix.py index d8056c862982..0d42a229bf25 100644 --- a/final_fix.py +++ b/final_fix.py @@ -1,31 +1,44 @@ import os -import re -def patch_file(path, old, new): - if not os.path.exists(path): - return +def fix_loc(): + path = 'osu.Game/Localisation/GraphicsSettingsStrings.cs' with open(path, 'r') as f: content = f.read() - if old in content: + + # Correct insertion before Resolution + insertion = '\n /// \n /// "Refresh rate"\n /// \n public static LocalisableString RefreshRate => new TranslatableString(getKey(@"refresh_rate"), @"Refresh rate");\n' + + # We use replace with exact match to ensure indentation is correct (8 spaces) + old_text = ' public static LocalisableString ScreenMode => new TranslatableString(getKey(@"screen_mode"), @"Screen mode");' + new_text = old_text + insertion + + if old_text in content and 'RefreshRate' not in content: with open(path, 'w') as f: - f.write(content.replace(old, new)) - else: - # Try regex if literal fails - new_content = re.sub(re.escape(old).replace(r'\ ', r'\s+'), new, content, flags=re.MULTILINE | re.DOTALL) - if new_content != content: - with open(path, 'w') as f: - f.write(new_content) - else: - print(f"Warning: '{old}' not found in {path}") - -# 1. Fix GameplayWarmupScreen IDE0074 (compound assignment) -gw_path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs' -old_gw = """ if (card == null) - { - // Played card was not on the screen. - - card = new RankedPlayCard(matchInfo.LastPlayedCard)""" -new_gw = """ card ??= new RankedPlayCard(matchInfo.LastPlayedCard) - { - // Played card was not on the screen.""" -# Wait, the braces are different. Let's look at the original code. + f.write(content.replace(old_text, new_text)) + print("Fixed Localisation") + +def fix_results(): + path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs' + with open(path, 'r') as f: + content = f.read() + + # Fix the if-null patterns using regex to preserve indentation exactly + import re + + # if (x != null) x.Looping = false; -> x?.Looping = false; + content = re.sub(r'if \((playerScoreTickChannel|opponentScoreTickChannel) != null\) \1\.Looping = false;', r'\1?.Looping = false;', content) + + # if (x != null && condition) -> if (condition) \n x?.Looping = false; + # Wait, the original was: + # if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent) + # playerScoreTickChannel.Looping = false; + + content = re.sub(r'if \((playerScoreTickChannel|opponentScoreTickChannel) != null && (.*?)\)\s+(.*?)\.Looping = false;', + r'if (\2)\n \1?.Looping = false;', content) + + with open(path, 'w') as f: + f.write(content) + print("Fixed ResultsScreen") + +fix_loc() +fix_results() diff --git a/fix_bot_feedback.py b/fix_bot_feedback.py deleted file mode 100644 index 7bffbc8696e3..000000000000 --- a/fix_bot_feedback.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys - -# 1. Fix DailyChallenge.cs -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'r') as f: - content = f.read() - -# Fix redundant conditional access -content = content.replace('if (item?.AllowedMods.Any() == true)', 'if (item.AllowedMods.Any())') - -# 2. Fix GameplayWarmupScreen.cs line breaks -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'r') as f: - gw_content = f.read() - -old_ternary = 'Children = beatmap == null ? System.Array.Empty() : [' -new_ternary = 'Children = beatmap == null\n ? System.Array.Empty()\n : [' - -gw_content = gw_content.replace(old_ternary, new_ternary) - -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'w') as f: - f.write(content) - -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'w') as f: - f.write(gw_content) diff --git a/fix_client_and_tests.py b/fix_client_and_tests.py deleted file mode 100644 index 7b568257df9a..000000000000 --- a/fix_client_and_tests.py +++ /dev/null @@ -1,54 +0,0 @@ -import re - -# 1. Update MultiplayerClient.cs to use UserID for LocalUser identification -# Also improve null safety in room setup. -with open('osu.Game/Online/Multiplayer/MultiplayerClient.cs', 'r') as f: - content = f.read() - -content = content.replace('public virtual MultiplayerRoomUser? LocalUser => Room?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.Id);', - 'public virtual MultiplayerRoomUser? LocalUser => Room?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.OnlineID);') - -with open('osu.Game/Online/Multiplayer/MultiplayerClient.cs', 'w') as f: - f.write(content) - -# 2. Update TestRoomRequestsHandler.cs to preserve RoomID, StartDate, and EndDate -with open('osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs', 'r') as f: - handler_content = f.read() - -old_clone_room = """ private Room cloneRoom(Room source) - { - var result = new Room(); - result.CopyFrom(source); - result.RoomID = source.RoomID; - result.StartDate = source.StartDate; - result.EndDate = source.EndDate; - result.Playlist = source.Playlist.Select(p => p.With()).ToList(); - return result; - }""" - -new_clone_room = """ private Room cloneRoom(Room source) - { - var result = new Room(); - result.CopyFrom(source); - result.RoomID = source.RoomID; - result.StartDate = source.StartDate; - result.EndDate = source.EndDate; - result.Host = source.Host; - result.Playlist = source.Playlist.Select(p => p.With()).ToList(); - return result; - }""" - -handler_content = handler_content.replace(old_clone_room, new_clone_room) - -with open('osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs', 'w') as f: - f.write(handler_content) - -# 3. Update TestScenePlayerPanelOverlay.cs assertions -with open('osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs', 'r') as f: - test_overlay_content = f.read() - -test_overlay_content = test_overlay_content.replace('AddAssert("no panels quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(0));', - 'AddAssert("no panels quit", () => list.Panels.Count(p => p.HasQuit), () => Is.EqualTo(0));') - -with open('osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs', 'w') as f: - f.write(test_overlay_content) diff --git a/fix_daily_challenge.py b/fix_daily_challenge.py deleted file mode 100644 index 84884c5ff520..000000000000 --- a/fix_daily_challenge.py +++ /dev/null @@ -1,120 +0,0 @@ -import sys - -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'r') as f: - content = f.read() - -# Fix presentScore -old_present_score = """ private void presentScore(long id) - { - if (!this.IsCurrentScreen()) - var item = playlistItem; - if (item == null) return; - - var item = playlistItem; - if (item != null) - this.Push(new PlaylistItemScoreResultsScreen(id, (room.RoomID ?? 0), item)); - }""" - -new_present_score = """ private void presentScore(long id) - { - if (!this.IsCurrentScreen()) - return; - - var item = playlistItem; - if (item == null) return; - - this.Push(new PlaylistItemScoreResultsScreen(id, (room.RoomID ?? 0), item)); - }""" - -# Fix updateMods -old_update_mods = """ private void updateMods() - { - var item = playlistItem; - if (item == null) return; - return; - - var item = playlistItem; - if (item != null) Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); - }""" - -new_update_mods = """ private void updateMods() - { - if (!this.IsCurrentScreen()) - return; - - var item = playlistItem; - if (item == null) return; - - Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); - }""" - -# Fix startPlay -old_start_play = """ private void startPlay() - { - sampleStart?.Play(); - var item = playlistItem; if (item != null) this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, item) - { - Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) - })); - }""" - -new_start_play = """ private void startPlay() - { - sampleStart?.Play(); - - var item = playlistItem; - if (item == null) return; - - this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, item) - { - Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) - })); - }""" - -# Fix PresentBeatmap -old_present_beatmap = """ public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) - { - var item = playlistItem; - if (item == null) return; - if (!this.IsCurrentScreen()) - return; - - var item = playlistItem; - - // We can only handle the current daily challenge beatmap. - // If the import was for a different beatmap, pass the duty off to global handling. - if (item?.Beatmap.BeatmapSet != null && beatmap.BeatmapSetInfo.OnlineID != item.Beatmap.BeatmapSet.OnlineID) - { - this.Exit(); - game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); - } - - // And if we're handling, we don't really have much to do here. - }""" - -new_present_beatmap = """ public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) - { - if (!this.IsCurrentScreen()) - return; - - var item = playlistItem; - if (item == null) return; - - // We can only handle the current daily challenge beatmap. - // If the import was for a different beatmap, pass the duty off to global handling. - if (item.Beatmap.BeatmapSet != null && beatmap.BeatmapSetInfo.OnlineID == item.Beatmap.BeatmapSet.OnlineID) - return; - - this.Exit(); - game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); - - // And if we're handling, we don't really have much to do here. - }""" - -content = content.replace(old_present_score, new_present_score) -content = content.replace(old_update_mods, new_update_mods) -content = content.replace(old_start_play, new_start_play) -content = content.replace(old_present_beatmap, new_present_beatmap) - -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'w') as f: - f.write(content) diff --git a/fix_daily_challenge_final.py b/fix_daily_challenge_final.py deleted file mode 100644 index e328a026f81b..000000000000 --- a/fix_daily_challenge_final.py +++ /dev/null @@ -1,45 +0,0 @@ -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'r') as f: - content = f.read() - -# Bot wants null propagation for: -# var item = playlistItem; -# if (item == null) return; -# This pattern is used in: presentScore, updateMods, startPlay, PresentBeatmap. - -# Since playlistItem is a field, we can use null-propagation directly in most cases. -# However, for startPlay and PresentBeatmap we need to perform actions. - -content = content.replace(''' private void presentScore(long id) - { - if (!this.IsCurrentScreen()) - return; - - var item = playlistItem; - if (item == null) return; - - this.Push(new PlaylistItemScoreResultsScreen(id, (room.RoomID ?? 0), item)); - }''', ''' private void presentScore(long id) - { - if (this.IsCurrentScreen() && playlistItem != null) - this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID ?? 0, playlistItem)); - }''') - -content = content.replace(''' private void updateMods() - { - if (!this.IsCurrentScreen()) - return; - - var item = playlistItem; - if (item == null) return; - - Mods.Value = userMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); - }''', ''' private void updateMods() - { - if (!this.IsCurrentScreen() || playlistItem == null) - return; - - Mods.Value = userMods.Value.Concat(playlistItem.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); - }''') - -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'w') as f: - f.write(content) diff --git a/fix_formatting.py b/fix_formatting.py index 78b8168df83f..369d5154bcc9 100644 --- a/fix_formatting.py +++ b/fix_formatting.py @@ -1,37 +1,18 @@ -import sys +import os -with open('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', 'r') as f: +path = 'osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs' +with open(path, 'r') as f: lines = f.readlines() -with open('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', 'w') as f: - for line in lines: - if 'orderedActiveItems = itemsByPriority' in line: - f.write(line) - continue - if '.OrderBy(i => i.priority)' in line: - f.write(' .OrderBy(i => i.priority)\n') - continue - if '.ThenBy(i => i.item.PlaylistOrder)' in line: - f.write(' .ThenBy(i => i.item.PlaylistOrder)\n') - continue - if '.ThenBy(i => i.item.ID)' in line: - f.write(' .ThenBy(i => i.item.ID)\n') - continue - if '.Select(i => i.item)' in line: - f.write(' .Select(i => i.item)\n') - continue - if '.ToList();' in line: - f.write(' .ToList();\n') - continue - f.write(line) +new_lines = [] +for line in lines: + # Look for the lines with formatting issues + if '.ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo)' in line: + # Just rewrite it exactly as it was, maybe it was a weird tab/space mix? + # Actually, let's look at the diff. + new_lines.append(line) + else: + new_lines.append(line) -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs', 'r') as f: - lines = f.readlines() - -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs', 'w') as f: - for line in lines: - if 'if (client.Room?.MatchState is not RankedPlayRoomState roomState) return;' in line: - f.write(' if (client.Room?.MatchState is not RankedPlayRoomState roomState)\n') - f.write(' return;\n') - continue - f.write(line) +with open(path, 'w') as f: + f.writelines(new_lines) diff --git a/fix_formatting_v3.py b/fix_formatting_v3.py deleted file mode 100644 index 2f22f064cdae..000000000000 --- a/fix_formatting_v3.py +++ /dev/null @@ -1,46 +0,0 @@ -import sys - -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'r') as f: - content = f.read() - -old_gw = """ if (card == null) - { - // Played card was not on the screen. - - card = new RankedPlayCard(matchInfo.LastPlayedCard) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - }""" - -new_gw = """ card ??= new RankedPlayCard(matchInfo.LastPlayedCard) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - };""" - -content = content.replace(old_gw, new_gw) -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'w') as f: - f.write(content) - -with open('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', 'r') as f: - lines = f.readlines() - -with open('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', 'w') as f: - skip = False - for i, line in enumerate(lines): - if 'private T clone(T incoming)' in line: - # Check if this is the duplicate one - if i > 810: # Rough estimate - f.write(' private T clone(T incoming)\n') - continue - if 'if (targetRoom.Host != null)' in line: - f.write(' if (targetRoom.Host != null)\n') - f.write(' targetRoom.Host.User = sourceRoom.Host?.User;\n') - skip = True - continue - if skip and 'targetRoom.Host.User = sourceRoom.Host?.User;' in line: - skip = False - continue - f.write(line) diff --git a/fix_gw_bot.py b/fix_gw_bot.py deleted file mode 100644 index 92f010dc2f95..000000000000 --- a/fix_gw_bot.py +++ /dev/null @@ -1,21 +0,0 @@ -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'r') as f: - content = f.read() - -# Bot mentioned: "InspectCode / Incorrect line breaks: Line break is missing elsewhere" -# Re-evaluating the Children = ... [ line. -# It might want the [ on the next line or indented differently. - -old_ternary = ''' Children = beatmap == null - ? System.Array.Empty() - : [ - new ShearAligningWrapper(new TitleWedge(beatmap))''' - -new_ternary = ''' Children = beatmap == null - ? System.Array.Empty() - : - [ - new ShearAligningWrapper(new TitleWedge(beatmap))''' - -content = content.replace(old_ternary, new_ternary) -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'w') as f: - f.write(content) diff --git a/fix_gw_bot_final.py b/fix_gw_bot_final.py deleted file mode 100644 index 4293ff28e43b..000000000000 --- a/fix_gw_bot_final.py +++ /dev/null @@ -1,28 +0,0 @@ -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'r') as f: - content = f.read() - -old_block = ''' [ - new ShearAligningWrapper(new TitleWedge(beatmap)) - { - Shear = -OsuGame.SHEAR, - }, - new ShearAligningWrapper(new MetadataWedge(beatmap)) - { - Shear = -OsuGame.SHEAR, - }, - ]''' - -new_block = ''' [ - new ShearAligningWrapper(new TitleWedge(beatmap)) - { - Shear = -OsuGame.SHEAR, - }, - new ShearAligningWrapper(new MetadataWedge(beatmap)) - { - Shear = -OsuGame.SHEAR, - }, - ]''' - -content = content.replace(old_block, new_block) -with open('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', 'w') as f: - f.write(content) diff --git a/fix_remaining.py b/fix_remaining.py deleted file mode 100644 index c11bd661d6a0..000000000000 --- a/fix_remaining.py +++ /dev/null @@ -1,40 +0,0 @@ -import re - -def patch_file(path, search, replacement): - with open(path, 'r') as f: - content = f.read() - new_content = content.replace(search, replacement) - if new_content == content: - print(f"Warning: No changes made to {path} using string match") - with open(path, 'w') as f: - f.write(new_content) - -# 1. AvatarOverlay null safety -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs', - 'public bool AddUser(APIUser user)\n {\n if (user == null || avatars.Any(a => a.User?.Id == user.Id))', - 'public bool AddUser(APIUser? user)\n {\n if (user == null || avatars.Any(a => a.User?.Id == user.Id))') - -# 2. GameplayWarmupScreen formatting and null safety -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', - 'private Drawable wedgesContainer = null!;\n\n [BackgroundDependencyLoader]', - 'private Drawable wedgesContainer = null!;\n\n [BackgroundDependencyLoader]') # Already correct maybe? - -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', - 'MultiplayerPlaylistItem item = Client.Room!.CurrentPlaylistItem;', - 'var item = Client.Room?.CurrentPlaylistItem;\n if (item == null) return;') - -# 3. DailyChallengeCarousel dot removal fix (ensuring it uses the index of drawable in content) -patch_file('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs', - 'int index = content.IndexOf(drawable);\n if (index >= 0)\n navigationFlow.Remove(navigationFlow[index], true);', - 'int index = content.IndexOf(drawable);\n if (index >= 0)\n navigationFlow.Remove(navigationFlow[index], true);') # Already done? - -# 4. Clean up DailyChallenge.cs (Ensure no double checks or weirdness) -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'r') as f: - dc_content = f.read() - -# Fix the duplicate check in onRoomScoreSet if it exists -dc_content = dc_content.replace('if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItemLocal?.ID)\n if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItemLocal?.ID)', - 'if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItemLocal?.ID)') - -with open('osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs', 'w') as f: - f.write(dc_content) diff --git a/fix_style.py b/fix_style.py deleted file mode 100644 index 029fc70645ec..000000000000 --- a/fix_style.py +++ /dev/null @@ -1,34 +0,0 @@ -import re - -def patch_file(path, search, replacement): - with open(path, 'r') as f: - content = f.read() - new_content = content.replace(search, replacement) - if new_content == content: - print(f"Warning: No changes made to {path}") - with open(path, 'w') as f: - f.write(new_content) - -# 1. RankedPlayMatchInfo formatting (IDE0055) and pattern matching (IDE0019) -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs', - 'var roomState = client.Room?.MatchState as RankedPlayRoomState;', - 'if (client.Room?.MatchState is not RankedPlayRoomState roomState)') - -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs', - 'if (client.Room?.MatchState is not RankedPlayRoomState roomState)\n if (roomState == null) return;', - 'if (client.Room?.MatchState is not RankedPlayRoomState roomState) return;') - -# 2. TestMultiplayerClient formatting (IDE0055) and simplification (IDE0031) -patch_file('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', - 'if (targetRoom.Host != null)\n targetRoom.Host.User = sourceRoom.Host?.User;', - 'if (targetRoom.Host != null)\n targetRoom.Host.User = sourceRoom.Host?.User;') # Placeholder - -# 3. PlayerPanelOverlay simplification (IDE0031) -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs', - 'var panel = panels.FirstOrDefault(p => p.RoomUser.Equals(user));\n if (panel != null) panel.HasQuit = true;', - 'if (panels.FirstOrDefault(p => p.RoomUser.Equals(user)) is PlayerPanel panel) panel.HasQuit = true;') - -# 4. GameplayWarmupScreen unnecessary using (IDE0005) -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', - 'using osu.Framework.Logging;', - '') diff --git a/fix_style_v2.py b/fix_style_v2.py deleted file mode 100644 index 48ed5b09b180..000000000000 --- a/fix_style_v2.py +++ /dev/null @@ -1,20 +0,0 @@ -import re - -def patch_file(path, search, replacement): - with open(path, 'r') as f: - content = f.read() - new_content = content.replace(search, replacement) - if new_content == content: - print(f"Warning: No changes made to {path}") - with open(path, 'w') as f: - f.write(new_content) - -# Fix IDE0031 in TestMultiplayerClient -patch_file('osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs', - 'if (targetRoom.Host != null)\n targetRoom.Host.User = sourceRoom.Host?.User;', - 'if (targetRoom.Host != null)\n targetRoom.Host.User = sourceRoom.Host?.User;') # Placeholder check - -# Ensure single line or proper wrapping to avoid IDE0055 -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs', - 'if (client.Room?.MatchState is not RankedPlayRoomState roomState) return;', - 'if (client.Room?.MatchState is not RankedPlayRoomState roomState)\n return;') diff --git a/fix_test_and_client.py b/fix_test_and_client.py deleted file mode 100644 index a544c0a1d4dc..000000000000 --- a/fix_test_and_client.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys - -# 1. Update MultiplayerClient.cs to use UserID for LocalUser identification -# and improve null safety in room setup. -with open('osu.Game/Online/Multiplayer/MultiplayerClient.cs', 'r') as f: - content = f.read() - -content = content.replace('public virtual MultiplayerRoomUser? LocalUser => Room?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.Id);', - 'public virtual MultiplayerRoomUser? LocalUser => Room?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.OnlineID);') - -with open('osu.Game/Online/Multiplayer/MultiplayerClient.cs', 'w') as f: - f.write(content) - -# 2. Update TestSceneMultiplayerPlaylist.cs to use correct IDs -with open('osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs', 'r') as f: - test_content = f.read() - -# The IDs in TestMultiplayerClient start at 1 and increment. -# The initial join creates ID 1. Subsequent adds create 2, 3, etc. -# In TestNonExpiredItemsAddedToQueueList: -# assertItemInQueueListStep(1, 0); // OK -# addItemStep(); // creates ID 2 -# assertItemInQueueListStep(2, 1); // OK -# addItemStep(); // creates ID 3 -# assertItemInQueueListStep(3, 2); // OK - -# The issue might be that RoomID or something else is causing a mismatch. -# Wait, looking at the logs: "1 in queue at pos = 0" timed out. -# This means ID 1 is not found at pos 0 in the Queue tab. - -# Let's check TestMultiplayerClient.cs again for ID generation. diff --git a/fix_ui_safety.py b/fix_ui_safety.py deleted file mode 100644 index 200fde386ae7..000000000000 --- a/fix_ui_safety.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -import re - -def patch_file(path, search_pattern, replacement): - with open(path, 'r') as f: - content = f.read() - new_content = re.sub(search_pattern, replacement, content, flags=re.MULTILINE | re.DOTALL) - if new_content == content: - print(f"Warning: No changes made to {path}") - with open(path, 'w') as f: - f.write(new_content) - -# GameplayWarmupScreen.cs safety and formatting -# Match current state from the read_file output -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs', - r'private Drawable wedgesContainer = null!;.*?\s+\[BackgroundDependencyLoader\]', - 'private Drawable wedgesContainer = null!;\n\n [BackgroundDependencyLoader]') - -# RankedPlayMatchInfo.cs safety -# The previous regex might have missed due to line breaks. -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs', - r'var roomState = \(RankedPlayRoomState\)client\.Room!\.MatchState!;\s+onMatchRoomStateChanged\(roomState\);', - 'var roomState = client.Room?.MatchState as RankedPlayRoomState;\n if (roomState == null) return;\n\n onMatchRoomStateChanged(roomState);') - -# PlayerPanelOverlay.cs safety -patch_file('osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs', - r'panels\.Single\(p => p\.RoomUser\.Equals\(user\)\)\.HasQuit = true;', - 'var panel = panels.FirstOrDefault(p => p.RoomUser.Equals(user));\n if (panel != null) panel.HasQuit = true;') diff --git a/global.json b/global.json new file mode 100644 index 000000000000..fe7e453b1cd8 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index d9051f6e1551..513521619ead 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -1,9 +1,9 @@ - + 33.0 - android-arm;android-arm64 + android-x86;android-arm;android-arm64 apk - CJK;West; + CJK;Mideast;Rare;West;Other; Xamarin.Android.Net.AndroidMessageHandler true diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 1eaf32f02f85..0e6b03cd6ebc 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -59,11 +59,6 @@ public class OsuGameActivity : AndroidGameActivity, ISurfaceHolderCallback private OsuGameAndroid? game; - protected OsuGameActivity(IntPtr handle, JniHandleOwnership transfer) - : base() - { - } - private bool gameCreated; protected override osu.Framework.Game CreateGame() diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj index 24749e7a934c..5d7c70e8fae0 100644 --- a/osu.Android/osu.Android.csproj +++ b/osu.Android/osu.Android.csproj @@ -31,5 +31,6 @@ + diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs index ab05fb8a8517..02407da7874c 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs @@ -12,13 +12,13 @@ namespace osu.Desktop.LegacyIpc /// In order to deserialise types at either end, types must be serialised as their , /// however this cannot be done since osu!stable and osu!lazer live in two different assemblies. ///
- /// To get around this, this class exists which serialises a payload () as an type, + /// To get around this, this class exists which serialises a payload () as an type, /// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib). /// The payload contains the data to be sent over the IPC channel. ///
- /// At either end, Json.NET deserialises the payload into a which is manually converted back into the expected type, + /// At either end, Json.NET deserialises the payload into a which is manually converted back into the expected type, /// which then further contains another representing the data sent over the IPC channel whose type can likewise be lazily matched through - /// . + /// . /// /// /// diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index 2ce71376f743..bd70094e9d95 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -51,7 +51,7 @@ protected override void OnFree() MissingStartTime.UnbindFrom(parentHold.MissingStartTime); } - public bool UpdateResult() => UpdateResult(true); + public bool UpdateResult() => base.UpdateResult(true); protected override void UpdateHitStateTransforms(ArmedState state) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 101a1def8d3f..f435fcacb30f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -53,7 +53,7 @@ protected override void OnFree() MissingStartTime.UnbindFrom(parentHold.MissingStartTime); } - public void UpdateResult() => UpdateResult(true); + public void UpdateResult() => base.UpdateResult(true); protected override void CheckForResult(bool userTriggered, double timeOffset) => // Factor in the release lenience diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 7f523505b7e9..12d884e29800 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -129,7 +129,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok /// By default, osu!mania beatmaps in only play samples at the start of the hold note. /// /// The object to use as a basis for the head sample. - /// Defaults for assigning to . + /// Defaults for assigning to . public static List> CreateDefaultNodeSamples(HitObject obj) => new List> { obj.Samples, diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs index 79785331aaa8..d8d9705530ae 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.UI { /// - /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield. + /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield. /// public partial class PlayfieldCoveringWrapper : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 68424763a5d9..79a6bb89e4a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -4,13 +4,15 @@ #nullable disable using System.Collections.Generic; + using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Objects; +using osu.Framework.Graphics; + using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { @@ -146,4 +148,4 @@ protected override void Dispose(bool isDisposing) lifetimeEntries.Clear(); } } -} +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 54f6176c315f..b97e0b93ca04 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -3,18 +3,18 @@ #nullable disable -using System; using System.Collections.Generic; -using JetBrains.Annotations; +using System; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; -using osuTK; +using JetBrains.Annotations; using osuTK.Graphics; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -206,4 +206,4 @@ internal void RestoreHitAnimations() #endregion } -} +} \ No newline at end of file diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index da10c15cd752..bf88cee11bae 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -52,7 +52,6 @@ public void TestDailyChallenge() AllowedMods = [new APIMod(new OsuModDoubleTime())] } ], - StartDate = DateTimeOffset.Now.AddSeconds(-10), EndDate = DateTimeOffset.Now.AddHours(12), Category = RoomCategory.DailyChallenge }; @@ -75,7 +74,6 @@ public void TestUseTheseModsUnavailableIfNoFreeMods() AllowedMods = [] } ], - StartDate = DateTimeOffset.Now.AddSeconds(-10), EndDate = DateTimeOffset.Now.AddHours(12), Category = RoomCategory.DailyChallenge }; @@ -108,19 +106,18 @@ public void TestNotifications() AllowedMods = [new APIMod(new OsuModDoubleTime())] } ], - StartDate = DateTimeOffset.Now.AddSeconds(-10), EndDate = DateTimeOffset.Now.AddHours(12), Category = RoomCategory.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = (room.RoomID ?? 0) }); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value }); Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); AddUntilStep("wait for screen", () => screen.IsCurrentScreen()); AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null); - AddUntilStep("notification posted", () => notificationOverlay.AllNotifications.OfType().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification)); + AddAssert("notification posted", () => notificationOverlay.AllNotifications.OfType().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification)); } [Test] @@ -137,13 +134,12 @@ public void TestConclusionNotificationDoesNotFireOnDisconnect() AllowedMods = [new APIMod(new OsuModDoubleTime())] } ], - StartDate = DateTimeOffset.Now.AddSeconds(-10), EndDate = DateTimeOffset.Now.AddHours(12), Category = RoomCategory.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = (room.RoomID ?? 0) }); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value }); Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs index e1986fa77bbd..eda596effb1e 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeEventFeed.cs @@ -78,7 +78,8 @@ public void TestBasicAppearance() CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), RNG.Next(11, 1000)); - + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); feed.AddNewScore(ev); }, 50); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index 740e77bbee63..97b957df4336 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -2,10 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Online.Metadata; @@ -19,6 +17,7 @@ using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Graphics; using osuTK.Input; +using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -30,7 +29,7 @@ public partial class TestSceneDailyChallengeIntro : OnlinePlayTestScene [Cached(typeof(INotificationOverlay))] private NotificationOverlay notificationOverlay = new NotificationOverlay(); - private Room? room; + private Room room = null!; [BackgroundDependencyLoader] private void load() @@ -45,53 +44,31 @@ private void load() [Test] public void TestDailyChallenge() { - startChallenge("first"); - AddUntilStep("wait for button room", () => this.ChildrenOfType().FirstOrDefault()?.Room?.RoomID == room?.RoomID); - AddStep("push screen", () => - { - if (room != null) - LoadScreen(new DailyChallengeIntro(room)); - }); + startChallenge(); + AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); } [Test] public void TestPlayIntroOnceFlag() { - startChallenge("first"); - AddUntilStep("wait for first button room", () => - { - var btn = this.ChildrenOfType().FirstOrDefault(); - return btn != null && btn.Room != null && btn.Room.RoomID == room?.RoomID; - }); - + startChallenge(); AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); - AddAssert("intro played flag is true", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); - startChallenge("second"); + startChallenge(); - AddUntilStep("wait for button to update to second room", () => - { - var btn = this.ChildrenOfType().FirstOrDefault(); - return btn != null && btn.Room != null && btn.Room.RoomID == room?.RoomID; - }); - AddUntilStep("intro played flag reset", () => !Dependencies.Get().Get(Static.DailyChallengeIntroPlayed)); + AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False); - AddStep("push screen", () => - { - if (room != null) - LoadScreen(new DailyChallengeIntro(room)); - }); + AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); + AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True); } - private void startChallenge(string suffix) + private void startChallenge() { - AddStep($"reset info ({suffix})", () => metadataClient.DailyChallengeUpdated(null!)); - AddStep($"reset room ({suffix})", () => room = null); - AddStep($"add room ({suffix})", () => + AddStep("add room", () => { - var newRoom = new Room + API.Perform(new CreateRoomRequest(room = new Room { - Name = $"Daily Challenge {suffix}", + Name = "Daily Challenge: June 4, 2024", Playlist = [ new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo)) @@ -100,20 +77,12 @@ private void startChallenge(string suffix) AllowedMods = [new APIMod(new OsuModDoubleTime())] } ], - StartDate = DateTimeOffset.Now.AddSeconds(-10), + StartDate = DateTimeOffset.Now, EndDate = DateTimeOffset.Now.AddHours(24), Category = RoomCategory.DailyChallenge - }; - room = newRoom; - API.Perform(new CreateRoomRequest(newRoom)); - }); - AddUntilStep($"wait for room id ({suffix})", () => room?.RoomID != null && room.RoomID > 0); - AddUntilStep($"wait for playlist id ({suffix})", () => room != null && room.Playlist.All(p => p.ID > 0)); - AddStep($"signal client ({suffix})", () => - { - if (room != null && room.RoomID.HasValue) - metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID.Value }); + })); }); + AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value })); } } } diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs index 8be82f50a98b..4619fad93898 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeTotalsDisplay.cs @@ -76,6 +76,8 @@ public void TestBasicAppearance() CoverUrl = TestResources.COVER_IMAGE_3, }, RNG.Next(1_000_000), RNG.Next(11, 1000)); + var testScore = TestResources.CreateTestScoreInfo(); + testScore.TotalScore = RNG.Next(1_000_000); totals.AddNewScore(ev); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 8b3247f1bd5b..6ac82005a71a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -648,7 +648,7 @@ private void load() } /// - /// Mutable dummy BatteryInfo class for + /// Mutable dummy BatteryInfo class for /// /// private class LocalBatteryInfo : BatteryInfo diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs index 2d3589dfa206..f41416925115 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking.Events; @@ -117,13 +118,13 @@ public void RemovePanels() }); }); - AddUntilStep("two panels displayed", () => list.Panels.Count(p => !p.HasQuit), () => Is.EqualTo(2)); - AddAssert("no panels quit", () => list.Panels.Count(p => p.HasQuit), () => Is.EqualTo(0)); + AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("no panels quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(0)); AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); - AddUntilStep("one panel quit", () => list.Panels.Count(p => p.HasQuit), () => Is.EqualTo(1)); - AddAssert("two panels still displayed", () => list.Panels.Count, () => Is.EqualTo(2)); + AddUntilStep("one panel quit", () => this.ChildrenOfType().Count(p => p.HasQuit), () => Is.EqualTo(1)); + AddAssert("two panels still displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 1a485ed77d2b..7513ea992ae7 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -275,7 +275,7 @@ public void TestSelectionNotLostWithConvertedBeatmapsShown() /// /// Note: This test was written to demonstrate the failure described at https://github.com/ppy/osu/issues/35023, /// but because the failure scenario there entailed a race condition, it was possible for the test to pass regardless - /// unless was increased. + /// unless was increased. /// [Test] public void TestPresentFromResults() diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index e3071dc00f0d..2c40d83e085a 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -105,7 +105,7 @@ protected override void LoadComplete() /// The updated beatmap model. public void Invalidate(IBeatmapInfo oldBeatmap, IBeatmapInfo newBeatmap) { - Invalidate(lookup => lookup.BeatmapInfo.Equals(oldBeatmap)); + base.Invalidate(lookup => lookup.BeatmapInfo.Equals(oldBeatmap)); lock (bindableUpdateLock) { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index caecf9d60947..265bf8db86ef 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -316,7 +316,7 @@ public List GetAllUsableBeatmapSets() /// /// Perform a lookup query on available s. - /// Use this overload instead of + /// Use this overload instead of /// when Realm is unable to transform an expression to the internal Realm query syntax. /// /// The query. diff --git a/osu.Game/Beatmaps/FlatWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs index 41bb50ffd368..c2505ec109ed 100644 --- a/osu.Game/Beatmaps/FlatWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatWorkingBeatmap.cs @@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps /// A which can be constructed directly from an .osu file (via ) /// or an instance (via , /// providing an implementation for - /// . + /// . /// public class FlatWorkingBeatmap : WorkingBeatmap { diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index 0dfed50e45ad..b3815569ecbc 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -8,7 +8,7 @@ namespace osu.Game.Beatmaps.Formats /// /// A built for difficulty calculation of legacy s /// - /// To use this, the decoder must be registered by the application through . + /// To use this, the decoder must be registered by the application through . /// Doing so will override any existing decoders. /// /// diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 8d9ec209a9ca..0875a60d7510 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -137,7 +137,7 @@ static double InverseDifficultyRange(double difficultyValue, double diff0, doubl } /// - /// Inverse function to . + /// Inverse function to . /// Maps a value returned by the function above back to the difficulty that produced it. /// /// The difficulty-dependent value to be unmapped. diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 7474e7cd4d4c..297d9bcbd248 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps /// - Access the storyboard via . /// - Access a local skin via . /// - Access the track via (and then for subsequent accesses). - /// - Create a playable via . + /// - Create a playable via . /// public interface IWorkingBeatmap { @@ -84,7 +84,7 @@ public interface IWorkingBeatmap /// /// By default, the beatmap load process will be interrupted after 10 seconds. /// For finer-grained control over the load process, use the - /// + /// /// overload instead. /// /// The to create a playable for. diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index bb45de4110ad..65ae42a3da22 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -12,7 +12,7 @@ namespace osu.Game.Database public static class RealmExtensions { /// - /// Performs a . + /// Performs a . /// If a match was not found, a is performed before trying a second time. /// This ensures that an instance is found even if the realm requested against was not in a consistent state. /// diff --git a/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs b/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs index 1e18754297b9..caed4b26b97d 100644 --- a/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs +++ b/osu.Game/Graphics/Containers/Markdown/Extensions/BlockAttributeExtension.cs @@ -9,11 +9,11 @@ namespace osu.Game.Graphics.Containers.Markdown.Extensions { /// - /// A variant of + /// A variant of /// which only handles generic attributes in the current markdown and ignores inline generic attributes. /// /// - /// For rationale, see implementation of . + /// For rationale, see implementation of . /// public class BlockAttributeExtension : IMarkdownExtension { diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 78c99773ba1f..0b5aa9050d9d 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -145,7 +145,7 @@ private WebSocketNotificationsClientConnector setUpNotificationsClient() private void onTokenChanged(ValueChangedEvent e) => config.SetValue(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); - void IAPIProvider.Schedule(Action action) => Schedule(action); + void IAPIProvider.Schedule(Action action) => base.Schedule(action); public string AccessToken => authentication.RequestAccessToken(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index fb2a2c706fc9..c01d0ca480a1 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -88,7 +88,7 @@ public virtual void Queue(APIRequest request) }); } - void IAPIProvider.Schedule(Action action) => Schedule(action); + void IAPIProvider.Schedule(Action action) => base.Schedule(action); public void Perform(APIRequest request) { diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index bf29068db400..560ac77f8f73 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -182,7 +182,7 @@ private set /// /// The corresponding to the local player, if available. /// - public virtual MultiplayerRoomUser? LocalUser => Room?.Users.FirstOrDefault(u => u.UserID == API.LocalUser.Value.OnlineID); + public virtual MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); /// /// Whether the is the host in . @@ -288,7 +288,7 @@ private async Task setupJoinedRoom(Room apiRoom, MultiplayerRoom joinedRoom, Can // Populate users. await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); if (joinedRoom.Host != null) - await PopulateUsers(new[] { joinedRoom.Host }).ConfigureAwait(false); + await PopulateUsers([joinedRoom.Host]).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => @@ -310,8 +310,8 @@ await runOnUpdateThreadAsync(() => // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; - var localUser = LocalUser; - if (localUser != null) addUserToAPIRoom(localUser); + Debug.Assert(LocalUser != null); + addUserToAPIRoom(LocalUser); foreach (var user in joinedRoom.Users) updateUserPlayingState(user.UserID, user.State); diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 3a55bc242e25..a24249d42c7f 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -147,8 +147,8 @@ public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, do } /// - /// Invokes , - /// refreshing and parameters for the . + /// Invokes , + /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index 4844e27373d6..c374fe315dcf 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -42,7 +42,7 @@ public interface IRulesetFilterCriteria /// /// /// For adding numerical-type range criteria, can be used for matching, - /// along with + /// along with /// and - and -typed overloads for parsing. /// /// diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 8c700cac1f60..3e70f52ee7ce 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Judgements { /// - /// A drawable object which visualises the hit result of a . + /// A drawable object which visualises the hit result of a . /// public partial class DrawableJudgement : PoolableDrawable { diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index e17556d51fab..ab83ee62b0a9 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -38,7 +38,7 @@ public class JudgementResult internal double? RawTime { get; set; } /// - /// The offset of from the end time of , clamped by . + /// The offset of from the end time of , clamped by . /// public double TimeOffset { @@ -47,7 +47,7 @@ public double TimeOffset } /// - /// The absolute time at which this occurred, clamped by the end time of plus . + /// The absolute time at which this occurred, clamped by the end time of plus . /// /// /// The end time of is returned if this result is not populated yet. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f950031424b8..694b223632ed 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -370,7 +370,7 @@ protected sealed override void OnFree(HitObjectLifetimeEntry entry) /// /// Invoked for this to take on any values from a newly-applied . - /// This is also fired after any changes which occurred via an call. + /// This is also fired after any changes which occurred via an call. /// protected virtual void OnApply() { @@ -378,7 +378,7 @@ protected virtual void OnApply() /// /// Invoked for this to revert any values previously taken on from the currently-applied . - /// This is also fired after any changes which occurred via an call. + /// This is also fired after any changes which occurred via an call. /// protected virtual void OnFree() { diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index d5eae49b4cf0..01d800a351a7 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -95,10 +95,10 @@ public static class HitEventExtensions public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit(); /// - /// Data type returned by which allows efficient incremental processing. + /// Data type returned by which allows efficient incremental processing. /// /// - /// This should be passed back into future calls as a parameter. + /// This should be passed back into future calls as a parameter. /// /// The optimisations used here rely on hit events being a consecutive sequence from a single gameplay session. /// When a new gameplay session is started, any existing results should be disposed. diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 4046279d2fd3..cf31f4c01b2a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -39,7 +39,7 @@ public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftD /// /// This property may be if the score was set on a beatmap (or a version of the beatmap) that is not available locally /// e.g. due to online updates, or local modifications to the beatmap. - /// The property will only link to a if its matches . + /// The property will only link to a if its matches . /// /// /// Due to the above, whenever setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed. @@ -54,7 +54,7 @@ public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftD public string ClientVersion { get; set; } = string.Empty; /// - /// The at the point in time when the score was set. + /// The at the point in time when the score was set. /// public string BeatmapHash { get; set; } = string.Empty; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c23f1b6b77be..d4c70d53df5c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { /// /// A container which provides a "blueprint" display of items. - /// Includes selection and manipulation support via a . + /// Includes selection and manipulation support via a . /// public abstract partial class BlueprintContainer : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler where T : class @@ -100,7 +100,7 @@ private void load() protected virtual SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; /// - /// Creates a which outlines items and handles movement of selections. + /// Creates a which outlines items and handles movement of selections. /// protected abstract SelectionHandler CreateSelectionHandler(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 4c6812ed43a5..b483f23d1d58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -209,7 +209,7 @@ private class TransformZoom : Transform private readonly float scrollOffset; /// - /// Transforms to a new value. + /// Transforms to a new value. /// /// The focus point in absolute coordinates local to the content. /// The size of the content. diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 747520116022..be22fc3c3085 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -149,12 +149,10 @@ private void dailyChallengeChanged(ValueChangedEvent _) } else { - if (info.Value is not DailyChallengeInfo infoValue) return; - var roomRequest = new GetRoomRequest(infoValue.RoomID); + var roomRequest = new GetRoomRequest(info.Value.Value.RoomID); roomRequest.Success += room => { - if (room == null) return; Room = room; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; @@ -166,7 +164,7 @@ private void dailyChallengeChanged(ValueChangedEvent _) statics.SetValue(Static.DailyChallengeIntroPlayed, false); // we only want to notify the user if the new challenge just went live. - if (room.StartDate != null && Math.Abs((DateTimeOffset.Now - (room.StartDate ?? DateTimeOffset.Now)).TotalSeconds) < 1800) + if (Math.Abs((DateTimeOffset.Now - room.StartDate.Value).TotalSeconds) < 1800) notificationOverlay?.Post(new NewDailyChallengeNotification(room)); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 2ee2cd35b7d8..15eefc10b69c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap { private readonly Room room; - private readonly PlaylistItem? playlistItem; + private readonly PlaylistItem playlistItem; /// /// Any mods applied by/to the local user. @@ -70,6 +70,7 @@ public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePres [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] private readonly DailyChallengeBeatmapAvailabilityTracker beatmapAvailabilityTracker; [Resolved] @@ -112,17 +113,10 @@ public DailyChallenge(Room room) { this.room = room; - playlistItem = room.Playlist.FirstOrDefault(); + playlistItem = room.Playlist.Single(); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; - beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(playlistItem ?? new PlaylistItem(new BeatmapInfo())); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(beatmapAvailabilityTracker); - return dependencies; + beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(playlistItem); } [BackgroundDependencyLoader] @@ -138,7 +132,7 @@ private void load(AudioManager audio) Children = new Drawable[] { beatmapAvailabilityTracker, - new ScreenStack(new RoomBackgroundScreen(playlistItem ?? new PlaylistItem(new BeatmapInfo()))) + new ScreenStack(new RoomBackgroundScreen(playlistItem)) { RelativeSizeAxes = Axes.Both, }, @@ -166,7 +160,7 @@ private void load(AudioManager audio) { new Drawable[] { - playlistItem == null ? new Container() : new DrawableRoomPlaylistItem(playlistItem, true) + new DrawableRoomPlaylistItem(playlistItem, true) { RelativeSizeAxes = Axes.X, AllowReordering = false, @@ -242,7 +236,7 @@ private void load(AudioManager audio) }, null, // Middle column (leaderboard) - leaderboard = new DailyChallengeLeaderboard(room, playlistItem ?? new PlaylistItem(new BeatmapInfo())) + leaderboard = new DailyChallengeLeaderboard(room, playlistItem) { RelativeSizeAxes = Axes.Both, PresentScore = presentScore, @@ -324,11 +318,7 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] IsValidMod = _ => false }); - var item = playlistItem; - - if (item == null) return; - - if (item.AllowedMods.Any()) + if (playlistItem.AllowedMods.Any()) { footerButtons.Insert(-1, new UserModSelectButton { @@ -340,8 +330,8 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] Action = () => userModsSelectOverlay.Show(), }); - var rulesetInstance = rulesets.GetRuleset(item.RulesetID)!.CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + var rulesetInstance = rulesets.GetRuleset(playlistItem.RulesetID)!.CreateInstance(); + var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)); userModsSelectOverlay.IsValidMod = leaderboard.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } @@ -353,14 +343,13 @@ [new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }] private void presentScore(long id) { - if (this.IsCurrentScreen() && playlistItem != null) - this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID ?? 0, playlistItem)); + if (this.IsCurrentScreen()) + this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem)); } private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) { - var playlistItemLocal = playlistItem; - if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItemLocal?.ID) + if (e.RoomID != room.RoomID || e.PlaylistItemID != playlistItem.ID) return; userLookupCache.GetUserAsync(e.UserID).ContinueWith(t => @@ -438,7 +427,7 @@ public override void OnEntering(ScreenTransitionEvent e) API.Queue(new JoinRoomRequest(room, null)); startLoopingTrack(this, musicController); - metadataClient.BeginWatchingMultiplayerRoom((room.RoomID ?? 0)).ContinueWith(t => + metadataClient.BeginWatchingMultiplayerRoom(room.RoomID!.Value).ContinueWith(t => { if (t.Exception != null) { @@ -447,8 +436,7 @@ public override void OnEntering(ScreenTransitionEvent e) } MultiplayerPlaylistItemStats[] stats = t.GetResultSafely(); - var playlistItemLocal = playlistItem; - var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItemLocal?.ID); + var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID); if (itemStats == null) return; @@ -491,14 +479,14 @@ public override bool OnExiting(ScreenExitEvent e) this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); API.Queue(new PartRoomRequest(room)); - metadataClient.EndWatchingMultiplayerRoom((room.RoomID ?? 0)).FireAndForget(); + metadataClient.EndWatchingMultiplayerRoom(room.RoomID!.Value).FireAndForget(); return base.OnExiting(e); } - public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem? item) + public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem item) { - if (item == null || !screen.IsCurrentScreen()) + if (!screen.IsCurrentScreen()) return; var beatmap = beatmaps.QueryOnlineBeatmapId(item.Beatmap.OnlineID); @@ -532,7 +520,7 @@ private void cancelTrackLooping() private void updateMods() { - if (!this.IsCurrentScreen() || playlistItem == null) + if (!this.IsCurrentScreen()) return; Mods.Value = userMods.Value.Concat(playlistItem.RequiredMods.Select(m => m.ToMod(Ruleset.Value.CreateInstance()))).ToList(); @@ -541,10 +529,6 @@ private void updateMods() private void startPlay() { sampleStart?.Play(); - - if (playlistItem == null) - return; - this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem) { Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) @@ -563,12 +547,12 @@ protected override void Dispose(bool isDisposing) public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) { - if (!this.IsCurrentScreen() || playlistItem == null) + if (!this.IsCurrentScreen()) return; // We can only handle the current daily challenge beatmap. // If the import was for a different beatmap, pass the duty off to global handling. - if (playlistItem.Beatmap.BeatmapSet != null && beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet.OnlineID) + if (beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet!.OnlineID) { this.Exit(); game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs index 89c4c0255b35..09c0c3f01746 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeCarousel.cs @@ -60,7 +60,8 @@ public override void Add(Drawable drawable) public override bool Remove(Drawable drawable, bool disposeImmediately) { int index = content.IndexOf(drawable); - if (index >= 0) + + if (index > 0) navigationFlow.Remove(navigationFlow[index], true); return base.Remove(drawable, disposeImmediately); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 53d08c920649..075d2af0aa83 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -35,7 +35,7 @@ public partial class DailyChallengeIntro : OsuScreen public override bool? ApplyModTrackAdjustments => true; private readonly Room room; - private readonly PlaylistItem? item; + private readonly PlaylistItem item; private Container introContent = null!; private Container topTitleDisplay = null!; @@ -56,6 +56,7 @@ public partial class DailyChallengeIntro : OsuScreen [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] private readonly DailyChallengeBeatmapAvailabilityTracker beatmapAvailabilityTracker; private bool shouldBePlayingMusic; @@ -87,22 +88,15 @@ public partial class DailyChallengeIntro : OsuScreen public DailyChallengeIntro(Room room) { this.room = room; - item = room.Playlist.FirstOrDefault(); + item = room.Playlist.Single(); ValidForResume = false; - beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(item ?? new PlaylistItem(new BeatmapInfo())); + beatmapAvailabilityTracker = new DailyChallengeBeatmapAvailabilityTracker(item); } protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider); - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(beatmapAvailabilityTracker); - return dependencies; - } - [BackgroundDependencyLoader] private void load(RulesetStore rulesets, BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio) { @@ -110,8 +104,6 @@ private void load(RulesetStore rulesets, BeatmapDifficultyCache difficultyCache, StarRatingDisplay starRatingDisplay; - if (item == null) return; - IBeatmapInfo beatmap = item.Beatmap; Ruleset ruleset = rulesets.GetRuleset(item.Beatmap.Ruleset.ShortName)!.CreateInstance(); @@ -361,7 +353,7 @@ public override void OnEntering(ScreenTransitionEvent e) beatmapAvailabilityTracker.Availability.BindValueChanged(availability => { if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable) - if (item != null) DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); + DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); }, true); this.FadeInFromZero(400, Easing.OutQuint); @@ -457,8 +449,8 @@ private void beginAnimation() Schedule(() => { shouldBePlayingMusic = true; - if (item != null) DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); - if (item != null) ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); + DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); + ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item); playBeatmapImpactSample(); }); } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 2cfc5aaaedd7..62c5c0c8dfb5 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -138,7 +138,7 @@ public void RefetchScores() if (request?.CompletionState == APIRequestCompletionState.Waiting) return; - request = new IndexPlaylistScoresRequest((room.RoomID ?? 0), playlistItem.ID); + request = new IndexPlaylistScoresRequest(room.RoomID!.Value, playlistItem.ID); request.Success += req => Schedule(() => { diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs index 0b31dbd52762..bf01ee6b522d 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeTimeRemainingRing.cs @@ -134,8 +134,8 @@ private void updateState() return; } - var roomDuration = (room.EndDate ?? DateTimeOffset.Now) - (room.StartDate ?? DateTimeOffset.Now); - var remaining = (room.EndDate ?? DateTimeOffset.Now) - DateTimeOffset.Now; + var roomDuration = room.EndDate.Value - room.StartDate.Value; + var remaining = room.EndDate.Value - DateTimeOffset.Now; timeText.Text = remaining.ToString(@"hh\:mm\:ss"); progress.Progress = remaining.TotalSeconds / roomDuration.TotalSeconds; diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 88e81640d9a7..32be5a8851a2 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -29,9 +29,7 @@ public NewDailyChallengeNotification(Room room) private void load(OsuGame? game, SessionStatics statics) { Text = DailyChallengeStrings.ChallengeLiveNotification; - var playlistItem = room.Playlist.FirstOrDefault(); - if (playlistItem != null) - Content.Add(card = new BeatmapCardNano((APIBeatmapSet)playlistItem.Beatmap.BeatmapSet!)); + Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { if (statics.Get(Static.DailyChallengeIntroPlayed)) @@ -46,7 +44,7 @@ private void load(OsuGame? game, SessionStatics statics) protected override void Update() { base.Update(); - card?.Width = Content.DrawWidth; + card.Width = Content.DrawWidth; } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs index e3115e56d1d0..48c64f2f6674 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContent.cs @@ -53,9 +53,9 @@ private void load(AudioManager audio) userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); } - public bool AddUser(APIUser? user) + public bool AddUser(APIUser user) { - if (user == null || avatars.Any(a => a.User?.Id == user.Id)) + if (avatars.Any(a => a.User.Id == user.Id)) return false; var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs index 55418a2eab46..e2d5fa7890bf 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/MatchmakingSelectPanel.CardContentBeatmap.cs @@ -92,7 +92,7 @@ private void load(OsuColour colours) thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true) { Name = @"Left (icon) area", - Size = new Vector2(HEIGHT), + Size = new Vector2(MatchmakingSelectPanel.HEIGHT), Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS }, Children = new Drawable[] { @@ -114,8 +114,8 @@ private void load(OsuColour colours) }, buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true) { - X = HEIGHT - BeatmapCard.CORNER_RADIUS, - Width = BeatmapCard.WIDTH - HEIGHT + BeatmapCard.CORNER_RADIUS, + X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS, + Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS, FavouriteState = { BindTarget = favouriteState }, ButtonsCollapsedWidth = 0, ButtonsExpandedWidth = 24, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index 2f0f830c01d2..ce14d0bb19c0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -24,8 +23,6 @@ public partial class PlayerPanelOverlay : CompositeDrawable [Resolved] private MultiplayerClient client { get; set; } = null!; - public IReadOnlyList Panels => panels.Children; - private Container panels = null!; private PlayerPanelCellContainer gridLayout = null!; private PlayerPanelCellContainer splitLayoutLeft = null!; @@ -114,8 +111,6 @@ public Drawable? DisplayArea private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => { - if (user.User == null) return; - panels.Add(new PlayerPanel(user) { Anchor = Anchor.Centre, @@ -128,8 +123,7 @@ private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => { - var panel = panels.FirstOrDefault(p => p.RoomUser.Equals(user)); - panel?.HasQuit = true; + panels.Single(p => p.RoomUser.Equals(user)).HasQuit = true; updateDisplay(); }); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs index bcd85d955a29..dc6cc06e9448 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs @@ -11,13 +11,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; - +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -72,15 +73,8 @@ public partial class GameplayWarmupScreen : RankedPlaySubScreen [BackgroundDependencyLoader] private void load() { - APIBeatmap? beatmap = null; - - var item = Client.Room?.CurrentPlaylistItem; - if (item != null) - { - beatmap = beatmapLookupCache.GetBeatmapAsync(item.BeatmapID).GetResultSafely(); - if (beatmap?.BeatmapSet != null) - lastLookupResult.Value = SongSelect.BeatmapSetLookupResult.Completed(beatmap.BeatmapSet); - } + APIBeatmap beatmap = beatmapLookupCache.GetBeatmapAsync(Client.Room!.CurrentPlaylistItem.BeatmapID).GetResultSafely()!; + lastLookupResult.Value = SongSelect.BeatmapSetLookupResult.Completed(beatmap.BeatmapSet); var matchState = Client.Room?.MatchState as RankedPlayRoomState; Debug.Assert(matchState != null); @@ -140,19 +134,17 @@ private void load() AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 4f), Direction = FillDirection.Vertical, - Children = beatmap == null - ? System.Array.Empty() - : - [ - new ShearAligningWrapper(new TitleWedge(beatmap)) - { - Shear = -OsuGame.SHEAR, - }, - new ShearAligningWrapper(new MetadataWedge(beatmap)) - { - Shear = -OsuGame.SHEAR, - }, - ] + Children = + [ + new ShearAligningWrapper(new TitleWedge(beatmap)) + { + Shear = -OsuGame.SHEAR, + }, + new ShearAligningWrapper(new MetadataWedge(beatmap)) + { + Shear = -OsuGame.SHEAR, + }, + ] } } } @@ -165,8 +157,7 @@ protected override void LoadComplete() { base.LoadComplete(); - var item = Client.Room?.CurrentPlaylistItem; - if (item == null) return; + MultiplayerPlaylistItem item = Client.Room!.CurrentPlaylistItem; RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = ruleset.CreateInstance(); @@ -209,11 +200,16 @@ public override void OnEntering(RankedPlaySubScreen? previous) } } - card ??= new RankedPlayCard(matchInfo.LastPlayedCard) + if (card == null) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; + Logger.Log($"Played card {matchInfo.LastPlayedCard.Card.ID} was not on the screen.", level: LogLevel.Error); + + card = new RankedPlayCard(matchInfo.LastPlayedCard) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } cardColumn.Add(card); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs index 42239aa4b6f4..657fbb13808d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs @@ -72,7 +72,7 @@ public partial class RankedPlayMatchInfo : Component public RankedPlayRoomState RoomState { get; private set; } = null!; - public bool IsOwnTurn => RoomState != null && client.LocalUser != null && RoomState.ActiveUserId == client.LocalUser.UserID; + public bool IsOwnTurn => RoomState.ActiveUserId == client.LocalUser?.UserID; public int CurrentRound => RoomState.CurrentRound; @@ -82,28 +82,23 @@ public partial class RankedPlayMatchInfo : Component private readonly List opponentCards = new List(); private readonly Bindable stage = new Bindable(); - private APIUser player = null!; - [Resolved] private MultiplayerClient client { get; set; } = null!; - + private APIUser player = null!; protected override void LoadComplete() { base.LoadComplete(); - var localUser = client.LocalUser; - if (localUser?.User != null) player = localUser.User; - else player = new APIUser { Id = localUser?.UserID ?? -1, Username = "Unknown" }; + player = client.LocalUser!.User!; client.MatchRoomStateChanged += onMatchRoomStateChanged; client.RankedPlayCardAdded += onCardAdded; client.RankedPlayCardRemoved += onCardRemoved; client.RankedPlayCardPlayed += onCardPlayed; - if (client.Room?.MatchState is not RankedPlayRoomState roomState) - return; + var roomState = (RankedPlayRoomState)client.Room!.MatchState!; onMatchRoomStateChanged(roomState); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 270b6a5002cc..a06aa633379f 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -45,8 +45,8 @@ public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable [Resolved] private SpectatorClient client { get; set; } = null!; - [Resolved(CanBeNull = true)] - private GameplayState? gameplayState { get; set; } = null!; + [Resolved] + private GameplayState gameplayState { get; set; } = null!; [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; @@ -87,7 +87,7 @@ protected override void LoadComplete() { base.LoadComplete(); - var state = gameplayState; if (state != null) ((IBindable)userPlayingState).BindTo(state.PlayingState); + ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index c759562c4243..61db282cdfc5 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -65,7 +65,7 @@ private void load(OsuGame? game, Player? player) { Task.Run(importFailedScore).ContinueWith(t => { - importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.ToLive(realm)); + importedScore = realm.Run?>(r => r.Find(t.GetResultSafely().ID)?.ToLive(realm)); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); }).FireAndForget(); } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index c04e0169bf20..8e899675c802 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -25,7 +25,7 @@ public class TrianglesSkin : Skin { public static SkinInfo CreateInfo() => new SkinInfo { - ID = Skinning.SkinInfo.TRIANGLES_SKIN, + ID = osu.Game.Skinning.SkinInfo.TRIANGLES_SKIN, Name = "osu! \"triangles\" (2017)", Creator = "team osu!", Protected = true, diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index 63c7ade234e8..262816ae89b7 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -17,7 +17,7 @@ public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDepende TestMultiplayerClient MultiplayerClient { get; } /// - /// The cached . + /// The cached . /// TestSpectatorClient SpectatorClient { get; } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index bbc493fed757..d1691d59ad01 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -65,7 +65,7 @@ public partial class TestMultiplayerClient : MultiplayerClient public new MultiplayerRoom Room => throw new InvalidOperationException($"Accessing the client-side room via {nameof(TestMultiplayerClient)} is unsafe. " + $"Use {nameof(ClientRoom)} if this was intended."); - public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.SingleOrDefault(u => u.UserID == API.LocalUser.Value.Id); + public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); public Action? RoomSetupAction; @@ -762,11 +762,7 @@ private async Task updatePlaylistOrder(MultiplayerRoom room) switch (room.Settings.QueueMode) { default: - orderedActiveItems = ServerRoom.Playlist - .Where(item => !item.Expired) - .OrderBy(item => item.PlaylistOrder) - .ThenBy(item => item.ID) - .ToList(); + orderedActiveItems = ServerRoom.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList(); break; case QueueMode.AllPlayersRoundRobin: @@ -780,8 +776,14 @@ private async Task updatePlaylistOrder(MultiplayerRoom room) } orderedActiveItems = itemsByPriority + // Order by each user's priority. .OrderBy(i => i.priority) + // Many users will have the same priority of items, so attempt to break the tie by maintaining previous ordering. + // Suppose there are two users: User1 and User2. User1 adds two items, and then User2 adds a third. If the previous order is not maintained, + // then after playing the first item by User1, their second item will become priority=0 and jump to the front of the queue (because it was added first). .ThenBy(i => i.item.PlaylistOrder) + // If there are still ties (normally shouldn't happen), break ties by making items added earlier go first. + // This could happen if e.g. the item orders get reset. .ThenBy(i => i.item.ID) .Select(i => i.item) .ToList(); @@ -809,25 +811,9 @@ private async Task updatePlaylistOrder(MultiplayerRoom room) private T clone(T incoming) { byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS); - var result = MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); - - if (incoming is MultiplayerRoomUser sourceUser && result is MultiplayerRoomUser targetUser) - targetUser.User = sourceUser.User; - - if (incoming is MultiplayerRoom sourceRoom && result is MultiplayerRoom targetRoom) - { - foreach (var user in targetRoom.Users) - user.User = sourceRoom.Users.FirstOrDefault(u => u.UserID == user.UserID)?.User; - - targetRoom.Host?.User = sourceRoom.Host?.User; - } - else if (incoming is MultiplayerRoomUser sourceSingleUser && result is MultiplayerRoomUser targetSingleUser) - { - targetSingleUser.User = sourceSingleUser.User; - } - - return result; + return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); } + public override Task DisconnectInternal() { isConnected.Value = false; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index d7efe2b6f34a..08f61f3ddcdf 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -3,34 +3,49 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Newtonsoft.Json; +using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Beatmaps; +using osu.Game.Utils; namespace osu.Game.Tests.Visual.OnlinePlay { - public interface IAPIRequestHandler + /// + /// Represents a handler which pretends to be a server, handling room retrieval and manipulation requests + /// and returning a roughly expected state, without the need for a server to be running. + /// + public class TestRoomRequestsHandler { - bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager); - } - - public class TestRoomRequestsHandler : IAPIRequestHandler - { - public List ServerSideRooms = new List(); - - private static long currentRoomId = 10000; - private static long currentPlaylistItemId = 10000; - private static long currentScoreId = 10000; - + public IReadOnlyList ServerSideRooms => serverSideRooms; + private readonly List serverSideRooms = new List(); + + private int currentRoomId = 1; + private int currentPlaylistItemId = 1; + private int currentScoreId = 1; + + /// + /// Handles an API request, while also updating the local state to match how the server would eventually respond. + /// + /// The API request to handle. + /// The local user to store in responses where required. + /// The beatmap manager to attempt to retrieve beatmaps from, prior to returning dummy beatmaps. + /// Whether the request was successfully handled. public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) { switch (request) { case CreateRoomRequest createRoomRequest: - { var apiRoom = cloneRoom(createRoomRequest.Room); // Passwords are explicitly not copied between rooms. @@ -39,23 +54,14 @@ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager AddServerSideRoom(apiRoom, localUser); var responseRoom = new APICreatedRoom(); - if (createResponseRoom(apiRoom, false) is Room res) - responseRoom.CopyFrom(res); - - // Propagate back to the source room object used by the test. - createRoomRequest.Room.RoomID = apiRoom.RoomID; - createRoomRequest.Room.StartDate = apiRoom.StartDate; - createRoomRequest.Room.EndDate = apiRoom.EndDate; - createRoomRequest.Room.Playlist = apiRoom.Playlist.Select(p => p.With()).ToList(); + responseRoom.CopyFrom(createResponseRoom(apiRoom, false)); createRoomRequest.TriggerSuccess(responseRoom); return true; - } case JoinRoomRequest joinRoomRequest: { - var room = ServerSideRooms.FirstOrDefault(r => r.RoomID == joinRoomRequest.Room.RoomID); - if (room == null) return false; + var room = ServerSideRooms.Single(r => r.RoomID == joinRoomRequest.Room.RoomID); if (joinRoomRequest.Password != room.Password) { @@ -63,31 +69,99 @@ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager return true; } - if (createResponseRoom(room, true) is Room joinRes) - joinRoomRequest.TriggerSuccess(joinRes); + joinRoomRequest.TriggerSuccess(createResponseRoom(room, true)); return true; } + case GetRoomLeaderboardRequest roomLeaderboardRequest: + roomLeaderboardRequest.TriggerSuccess(new APILeaderboard + { + Leaderboard = new List + { + new APIUserScoreAggregate + { + TotalScore = 1000000, + TotalAttempts = 5, + CompletedBeatmaps = 2, + User = new APIUser { Username = "best user" } + }, + new APIUserScoreAggregate + { + TotalScore = 50, + TotalAttempts = 1, + CompletedBeatmaps = 1, + User = new APIUser { Username = "worst user" } + } + } + }); + return true; + + case IndexPlaylistScoresRequest roomLeaderboardRequest: + roomLeaderboardRequest.TriggerSuccess(new IndexedMultiplayerScores + { + Scores = + { + new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + Position = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = new APIUser { Username = "best user" }, + Mods = [new APIMod { Acronym = @"DT" }], + Statistics = new Dictionary() + }, + new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 0.7, + Position = 2, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.B, + MaxCombo = 100, + TotalScore = 200000, + User = new APIUser { Username = "worst user" }, + Mods = [new APIMod { Acronym = @"TD" }], + Statistics = new Dictionary() + }, + }, + UserScore = new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 0.91, + Position = 4, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.A, + MaxCombo = 100, + TotalScore = 800000, + User = localUser, + Statistics = new Dictionary() + }, + }); + return true; + + case PartRoomRequest partRoomRequest: + partRoomRequest.TriggerSuccess(); + return true; + case GetRoomsRequest getRoomsRequest: - { var roomsWithoutParticipants = new List(); foreach (var r in ServerSideRooms) - { - if (createResponseRoom(r, false) is Room roomsRes) - roomsWithoutParticipants.Add(roomsRes); - } + roomsWithoutParticipants.Add(createResponseRoom(r, false)); getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); return true; - } case GetRoomRequest getRoomRequest: - { - if (createResponseRoom(ServerSideRooms.FirstOrDefault(r => r.RoomID == getRoomRequest.RoomId), true) is Room getRes) - getRoomRequest.TriggerSuccess(getRes); + getRoomRequest.TriggerSuccess(createResponseRoom(ServerSideRooms.Single(r => r.RoomID == getRoomRequest.RoomId), true)); return true; - } case CreateRoomScoreRequest createRoomScoreRequest: createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); @@ -97,102 +171,145 @@ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore { ID = currentScoreId++, + Accuracy = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, User = localUser, - Rank = Scoring.ScoreRank.S, + Statistics = new Dictionary() }); return true; - case GetRoomLeaderboardRequest getRoomLeaderboardRequest: - getRoomLeaderboardRequest.TriggerSuccess(new APILeaderboard + case GetBeatmapRequest getBeatmapRequest: + { + getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.OnlineID).Single()); + return true; + } + + case GetBeatmapsRequest getBeatmapsRequest: + { + getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = createResponseBeatmaps(getBeatmapsRequest.BeatmapIds.ToArray()) }); + return true; + } + + case GetBeatmapSetRequest getBeatmapSetRequest: + { + var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId + ? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID) + : beatmapManager.QueryBeatmapSet(s => s.OnlineID == getBeatmapSetRequest.ID)?.PerformRead(s => s.Beatmaps.First().Detach()); + + if (baseBeatmap == null) { - Leaderboard = - [ - new APIUserScoreAggregate - { - User = localUser, - Accuracy = 1, - TotalScore = 1000000, - } - ] - }); + baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; + baseBeatmap.OnlineID = getBeatmapSetRequest.ID; + baseBeatmap.BeatmapSet!.OnlineID = getBeatmapSetRequest.ID; + } + + getBeatmapSetRequest.TriggerSuccess(OsuTestScene.CreateAPIBeatmapSet(baseBeatmap)); return true; + } - case IndexPlaylistScoresRequest indexPlaylistScoresRequest: - indexPlaylistScoresRequest.TriggerSuccess(new IndexedMultiplayerScores + case GetUsersRequest getUsersRequest: + { + getUsersRequest.TriggerSuccess(new GetUsersResponse { - Scores = - [ - new MultiplayerScore - { - ID = currentScoreId++, - User = localUser, - Rank = Scoring.ScoreRank.S, - } - ], - UserScore = new MultiplayerScore - { - ID = currentScoreId++, - User = localUser, - Rank = Scoring.ScoreRank.A, - } + Users = getUsersRequest.UserIds.Select(id => id == TestUserLookupCache.UNRESOLVED_USER_ID + ? null + : new APIUser + { + Id = id, + Username = $"User {id}", + Team = RNG.NextBool() + ? new APITeam + { + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/flag/1/wanglogo.jpg", + } + : null, + }) + .Where(u => u != null).ToList(), }); return true; + } + } - case GetBeatmapRequest getBeatmapRequest: + List createResponseBeatmaps(params int[] beatmapIds) + { + var result = new List(); + + foreach (int id in beatmapIds) { - if (createResponseBeatmaps(getBeatmapRequest.OnlineID).FirstOrDefault() is APIBeatmap bm) - getBeatmapRequest.TriggerSuccess(bm); - return true; + var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id); + + if (baseBeatmap == null) + { + baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo; + baseBeatmap.OnlineID = id; + baseBeatmap.BeatmapSet!.OnlineID = id; + } + + result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap)); } + + return result; } return false; } - public void AddServerSideRoom(Room room, APIUser user) + /// + /// Adds a room to a local "server-side" list that's returned when a is fired. + /// + /// The room. + /// The room host. + public void AddServerSideRoom(Room room, APIUser host) { room.RoomID = currentRoomId++; - room.Host = user; - - if (room.StartDate == null) - room.StartDate = DateTimeOffset.Now; + room.Host = host; - foreach (var item in room.Playlist) + for (int i = 0; i < room.Playlist.Count; i++) { - if (item.ID == 0) - item.ID = currentPlaylistItemId++; + room.Playlist[i].ID = currentPlaylistItemId++; + room.Playlist[i].OwnerID = room.Host.OnlineID; } - ServerSideRooms.Add(room); + serverSideRooms.Add(room); } - private Room cloneRoom(Room source) + private Room createResponseRoom(Room room, bool withParticipants) { - var result = new Room(); - result.CopyFrom(source); - result.RoomID = source.RoomID; - result.StartDate = source.StartDate; - result.EndDate = source.EndDate; - result.Host = source.Host; - result.Playlist = source.Playlist.Select(p => p.With()).ToList(); - return result; - } - - private Room? createResponseRoom(Room? room, bool withParticipants) - { - if (room == null) return null; - var responseRoom = cloneRoom(room); + // Password is hidden from the response, and is only propagated via HasPassword. + responseRoom.Password = responseRoom.HasPassword ? Guid.NewGuid().ToString() : null; + if (!withParticipants) - responseRoom.ParticipantCount = 0; + responseRoom.RecentParticipants = []; return responseRoom; } - private IEnumerable createResponseBeatmaps(int onlineID) + private Room cloneRoom(Room source) { - yield return new APIBeatmap { OnlineID = onlineID }; + var result = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); + Debug.Assert(result != null); + + // When serialising, only beatmap IDs are sent to the server. + // When deserialising, full beatmaps and IDs are expected to arrive. + + PlaylistItem? finalCurrentItem = result.CurrentPlaylistItem?.With(id: source.CurrentPlaylistItem!.ID, beatmap: new Optional(source.CurrentPlaylistItem.Beatmap)); + PlaylistItem[] finalPlaylist = result.Playlist.Select((pi, i) => pi.With(id: source.Playlist[i].ID, beatmap: new Optional(source.Playlist[i].Beatmap))).ToArray(); + + // When setting the properties, we do a clear-then-add, otherwise equality comparers (that only compare by ID) pass early and members don't get replaced. + result.CurrentPlaylistItem = null; + result.CurrentPlaylistItem = finalCurrentItem; + result.Playlist = []; + result.Playlist = finalPlaylist; + + return result; } } } diff --git a/osu.Game/Utils/TagLibUtils.cs b/osu.Game/Utils/TagLibUtils.cs index 9e0eb27a1d72..4989d0423820 100644 --- a/osu.Game/Utils/TagLibUtils.cs +++ b/osu.Game/Utils/TagLibUtils.cs @@ -11,9 +11,9 @@ namespace osu.Game.Utils public class TagLibUtils { /// - /// Creates a with culture-invariant MIME type detection, based on stream data. + /// Creates a with culture-invariant MIME type detection, based on stream data. /// - /// The created. + /// The created. public static File GetTagLibFile(string filename, Stream stream) { var fileAbstraction = new StreamFileAbstraction(filename, stream); @@ -22,10 +22,10 @@ public static File GetTagLibFile(string filename, Stream stream) } /// - /// Creates a with culture-invariant MIME type detection based on a file on disk. + /// Creates a with culture-invariant MIME type detection based on a file on disk. /// /// The full path of the file to be created. - /// The created. + /// The created. public static File GetTagLibFile(string filePath) => File.Create(filePath, getMimeType(filePath), ReadStyle.Average | ReadStyle.PictureLazy);