diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index c5302a393c..8c9741b98b 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -36,6 +36,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } + public int APIVersion => 20220217; // We may want to pull this from the game version eventually. + public Exception LastLoginError { get; private set; } public string ProvidedUsername { get; private set; } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 91148c177f..776ff5fd8f 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.IO.Network; @@ -112,6 +113,9 @@ namespace osu.Game.Online.API WebRequest = CreateWebRequest(); WebRequest.Failed += Fail; WebRequest.AllowRetryOnTimeout = false; + + WebRequest.AddHeader("x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrEmpty(API.AccessToken)) WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}"); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7131c3a7d4..f292e95bd1 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -33,6 +33,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl => "http://localhost"; + public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); + public Exception LastLoginError { get; private set; } /// diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index a97eae77e3..470d46cd7f 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -57,6 +57,11 @@ namespace osu.Game.Online.API /// string WebsiteRootUrl { get; } + /// + /// The version of the API. + /// + int APIVersion { get; } + /// /// The last login error that occurred, if any. /// diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 2b64e5de06..a53ac1cd9b 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -12,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { + [JsonObject(MemberSerialization.OptIn)] public class APIUser : IEquatable, IUser { [JsonProperty(@"id")] diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 33718f050b..f696362cbb 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -62,6 +62,10 @@ namespace osu.Game.Online.Rooms [JsonProperty("beatmap_id")] private int onlineBeatmapId => Beatmap.OnlineID; + /// + /// A beatmap representing this playlist item. + /// In many cases, this will *not* contain any usable information apart from OnlineID. + /// [JsonIgnore] public IBeatmapInfo Beatmap { get; set; } = null!; diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index a328f8e8c0..a33150fe08 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -15,6 +14,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Rooms { + [JsonObject(MemberSerialization.OptIn)] public class Room : IDeepCloneable { [Cached] @@ -37,8 +37,19 @@ namespace osu.Game.Online.Rooms [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); + [JsonProperty("current_playlist_item")] + [Cached] + public readonly Bindable CurrentPlaylistItem = new Bindable(); + + [JsonProperty("playlist_item_stats")] + [Cached] + public readonly Bindable PlaylistItemStats = new Bindable(); + + [JsonProperty("difficulty_range")] + [Cached] + public readonly Bindable DifficultyRange = new Bindable(); + [Cached] - [JsonIgnore] public readonly Bindable Category = new Bindable(); // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) @@ -51,19 +62,15 @@ namespace osu.Game.Online.Rooms } [Cached] - [JsonIgnore] public readonly Bindable MaxAttempts = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Status = new Bindable(new RoomStatusOpen()); [Cached] - [JsonIgnore] public readonly Bindable Availability = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Type = new Bindable(); // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) @@ -76,7 +83,6 @@ namespace osu.Game.Online.Rooms } [Cached] - [JsonIgnore] public readonly Bindable QueueMode = new Bindable(); [JsonConverter(typeof(SnakeCaseStringEnumConverter))] @@ -88,7 +94,6 @@ namespace osu.Game.Online.Rooms } [Cached] - [JsonIgnore] public readonly Bindable MaxParticipants = new Bindable(); [Cached] @@ -113,7 +118,6 @@ namespace osu.Game.Online.Rooms public readonly Bindable Password = new Bindable(); [Cached] - [JsonIgnore] public readonly Bindable Duration = new Bindable(); [JsonProperty("duration")] @@ -158,6 +162,8 @@ namespace osu.Game.Online.Rooms var copy = new Room(); copy.CopyFrom(this); + + // ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not. copy.RoomID.Value = null; return copy; @@ -183,6 +189,9 @@ namespace osu.Game.Online.Rooms EndDate.Value = other.EndDate.Value; UserScore.Value = other.UserScore.Value; QueueMode.Value = other.QueueMode.Value; + DifficultyRange.Value = other.DifficultyRange.Value; + PlaylistItemStats.Value = other.PlaylistItemStats.Value; + CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); @@ -211,21 +220,27 @@ namespace osu.Game.Online.Rooms Playlist.RemoveAll(i => i.Expired); } - #region Newtonsoft.Json implicit ShouldSerialize() methods + [JsonObject(MemberSerialization.OptIn)] + public class RoomPlaylistItemStats + { + [JsonProperty("count_active")] + public int CountActive; - // The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases. - // They rely on being named exactly the same as the corresponding fields (casing included) and as such should NOT be renamed - // unless the fields are also renamed. + [JsonProperty("count_total")] + public int CountTotal; - [UsedImplicitly] - public bool ShouldSerializeRoomID() => false; + [JsonProperty("ruleset_ids")] + public int[] RulesetIDs; + } - [UsedImplicitly] - public bool ShouldSerializeHost() => false; + [JsonObject(MemberSerialization.OptIn)] + public class RoomDifficultyRange + { + [JsonProperty("min")] + public double Min; - [UsedImplicitly] - public bool ShouldSerializeEndDate() => false; - - #endregion + [JsonProperty("max")] + public double Max; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index d46ff12279..2faa46e622 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -23,6 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { InternalChild = sprite = CreateBackgroundSprite(); + CurrentPlaylistItem.BindValueChanged(_ => updateBeatmap()); Playlist.CollectionChanged += (_, __) => updateBeatmap(); updateBeatmap(); @@ -30,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { - sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap; + sprite.Beatmap.Value = CurrentPlaylistItem.Value?.Beatmap ?? Playlist.GetCurrentItem()?.Beatmap; } protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index 95ecadd21a..7425e46bd3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -75,15 +74,29 @@ namespace osu.Game.Screens.OnlinePlay.Components { base.LoadComplete(); - Playlist.BindCollectionChanged(updateRange, true); + DifficultyRange.BindValueChanged(_ => updateRange()); + Playlist.BindCollectionChanged((_, __) => updateRange(), true); } - private void updateRange(object sender, NotifyCollectionChangedEventArgs e) + private void updateRange() { - var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); + StarDifficulty minDifficulty; + StarDifficulty maxDifficulty; - StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); - StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); + if (DifficultyRange.Value != null) + { + minDifficulty = new StarDifficulty(DifficultyRange.Value.Min, 0); + maxDifficulty = new StarDifficulty(DifficultyRange.Value.Max, 0); + } + else + { + // In multiplayer rooms, the beatmaps of playlist items will not be populated to a point this can be correct. + // Either populating them via BeatmapLookupCache or polling the API for the room's DifficultyRange will be required. + var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray(); + + minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0); + maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarRating : 0, 0); + } minDisplay.Current.Value = minDifficulty; maxDisplay.Current.Value = maxDifficulty; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index a1a82c907a..5adce862a0 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -388,7 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override void LoadComplete() { base.LoadComplete(); - SelectedItem.BindValueChanged(onSelectedItemChanged, true); + CurrentPlaylistItem.BindValueChanged(onSelectedItemChanged, true); } private CancellationTokenSource beatmapLookupCancellation; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index ef2c2df4a6..a6bbcd548d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; +using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; @@ -41,15 +41,22 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - Playlist.BindCollectionChanged(updateCount, true); + PlaylistItemStats.BindValueChanged(_ => updateCount()); + Playlist.BindCollectionChanged((_, __) => updateCount(), true); } - private void updateCount(object sender, NotifyCollectionChangedEventArgs e) + private void updateCount() { + int activeItems = Playlist.Count > 0 || PlaylistItemStats.Value == null + // For now, use the playlist as the source of truth if it has any items. + // This allows the count to display correctly on the room screen (after joining a room). + ? Playlist.Count(i => !i.Expired) + : PlaylistItemStats.Value.CountActive; + count.Clear(); - count.AddText(Playlist.Count.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); + count.AddText(activeItems.ToLocalisableString(), s => s.Font = s.Font.With(weight: FontWeight.Bold)); count.AddText(" "); - count.AddText("Beatmap".ToQuantity(Playlist.Count, ShowQuantityAs.None)); + count.AddText("Beatmap".ToQuantity(activeItems, ShowQuantityAs.None)); } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 3260427192..175cd2c44e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.RulesetID == criteria.Ruleset.OnlineID); + matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats.Value?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 7f1db733b3..be98a9d4e9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -343,7 +343,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match base.LoadComplete(); drawablePlaylist.Items.BindTo(Playlist); - drawablePlaylist.SelectedItem.BindTo(SelectedItem); + drawablePlaylist.SelectedItem.BindTo(CurrentPlaylistItem); } protected override void Update() @@ -419,7 +419,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) { ErrorText.Text = "The selected beatmap is not available online."; - SelectedItem.Value.MarkInvalid(); + CurrentPlaylistItem.Value.MarkInvalid(); } else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 06959d942f..023af85f3b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => updateState()); + CurrentPlaylistItem.BindValueChanged(_ => updateState()); } protected override void OnRoomUpdated() @@ -111,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match bool enableButton = Room?.State == MultiplayerRoomState.Open - && SelectedItem.Value?.ID == Room.Settings.PlaylistItemId + && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 7b90532cce..eeafebfec0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -52,14 +52,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem }, + SelectedItem = { BindTarget = CurrentPlaylistItem }, RequestEdit = item => RequestEdit?.Invoke(item) }, historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both, Alpha = 0, - SelectedItem = { BindTarget = SelectedItem } + SelectedItem = { BindTarget = CurrentPlaylistItem } } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index c833621fbc..95d9b2af15 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -32,9 +32,22 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Type { get; private set; } + /// + /// The currently selected item in the , or the current item from + /// if this is not within a . + /// + [Resolved(typeof(Room))] + protected Bindable CurrentPlaylistItem { get; private set; } + + [Resolved(typeof(Room))] + protected Bindable PlaylistItemStats { get; private set; } + [Resolved(typeof(Room))] protected BindableList Playlist { get; private set; } + [Resolved(typeof(Room))] + protected Bindable DifficultyRange { get; private set; } + [Resolved(typeof(Room))] protected Bindable Category { get; private set; } @@ -71,12 +84,6 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private IBindable subScreenSelectedItem { get; set; } - /// - /// The currently selected item in the , or the current item from - /// if this is not within a . - /// - protected readonly Bindable SelectedItem = new Bindable(); - protected override void LoadComplete() { base.LoadComplete(); @@ -85,9 +92,13 @@ namespace osu.Game.Screens.OnlinePlay Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); } - protected virtual void UpdateSelectedItem() - => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null - ? Playlist.GetCurrentItem() - : subScreenSelectedItem.Value; + protected void UpdateSelectedItem() + { + // null room ID means this is a room in the process of being created. + if (RoomID.Value == null) + CurrentPlaylistItem.Value = Playlist.GetCurrentItem(); + else if (subScreenSelectedItem != null) + CurrentPlaylistItem.Value = subScreenSelectedItem.Value; + } } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 6abcb2924c..3de4e7afd9 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -43,6 +43,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (ruleset != null) { + room.PlaylistItemStats.Value = new Room.RoomPlaylistItemStats + { + RulesetIDs = new[] { ruleset.OnlineID }, + }; + room.Playlist.Add(new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID,