diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1771459a878b..f9f8ce3eea2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,8 +182,6 @@ jobs: uses: actions/setup-dotnet@v5 with: dotnet-version: "10.0.x" - - name: Set Xcode version - run: sudo xcode-select -s /Applications/Xcode_26.3.app - name: Install .NET Workloads run: dotnet workload install ios 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 c715105e01bf..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,45 +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", () => this.ChildrenOfType().FirstOrDefault()?.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", () => this.ChildrenOfType().FirstOrDefault()?.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)) @@ -92,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 is > 0); - AddUntilStep($"wait for playlist id ({suffix})", () => room?.Playlist.All(p => p.ID > 0) is true); - AddStep($"signal client ({suffix})", () => - { - if (room?.RoomID is long roomId) - metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }); + })); }); + 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/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 462bed398745..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.Id); + 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/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 480572f152ac..be22fc3c3085 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -149,9 +149,7 @@ 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 => { @@ -166,14 +164,13 @@ 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)); } updateCountdown(); Scheduler.AddDelayed(updateCountdown, 1000, true); }; - api.Queue(roomRequest); } } 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 00fb5c201c1b..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(); @@ -360,7 +352,7 @@ public override void OnEntering(ScreenTransitionEvent e) beatmapAvailabilityTracker.Availability.BindValueChanged(availability => { - if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable && item != null) + if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable) DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item); }, true); @@ -457,11 +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 25f1c7465ca3..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)) 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 6882ddabe858..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 660f64b45b67..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,16 +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); @@ -141,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, + }, + ] } } } @@ -166,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(); @@ -210,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 cba7ab4f1620..657fbb13808d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlayMatchInfo.cs @@ -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 6e21536e553b..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; } + [Resolved] + private GameplayState gameplayState { get; set; } = null!; [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; @@ -87,10 +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/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 4faea505b930..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.FirstOrDefault(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,24 +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 { User: { } } 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 95c7b8355c9c..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,107 +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, - }, - new APIUserScoreAggregate - { - User = new APIUser { Username = "other user" }, - Accuracy = 0.5, - TotalScore = 500000, - } - ] - }); + 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; - - 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; } } }