// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.IO.Serialization.Converters; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Utils; namespace osu.Game.Online.Rooms { public class Room : IDeepCloneable { [Cached] [JsonProperty("id")] public readonly Bindable RoomID = new Bindable(); [Cached] [JsonProperty("name")] public readonly Bindable Name = new Bindable(); [Cached] [JsonProperty("host")] public readonly Bindable Host = new Bindable(); [Cached] [JsonProperty("playlist")] public readonly BindableList Playlist = new BindableList(); [Cached] [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")] 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) [JsonProperty("category")] [JsonConverter(typeof(SnakeCaseStringEnumConverter))] private RoomCategory category { get => Category.Value; set => Category.Value = value; } [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) [JsonConverter(typeof(SnakeCaseStringEnumConverter))] [JsonProperty("type")] private MatchType type { get => Type.Value; set => Type.Value = value; } [Cached] [JsonIgnore] public readonly Bindable QueueMode = new Bindable(); [JsonConverter(typeof(SnakeCaseStringEnumConverter))] [JsonProperty("queue_mode")] private QueueMode queueMode { get => QueueMode.Value; set => QueueMode.Value = value; } [Cached] [JsonIgnore] public readonly Bindable MaxParticipants = new Bindable(); [Cached] [JsonProperty("current_user_score")] public readonly Bindable UserScore = new Bindable(); [JsonProperty("has_password")] public readonly BindableBool HasPassword = new BindableBool(); [Cached] [JsonProperty("recent_participants")] public readonly BindableList RecentParticipants = new BindableList(); [Cached] [JsonProperty("participant_count")] public readonly Bindable ParticipantCount = new Bindable(); #region Properties only used for room creation request [Cached(Name = nameof(Password))] [JsonProperty("password")] public readonly Bindable Password = new Bindable(); [Cached] [JsonIgnore] public readonly Bindable Duration = new Bindable(); [JsonProperty("duration")] private int? duration { get => (int?)Duration.Value?.TotalMinutes; set { if (value == null) Duration.Value = null; else Duration.Value = TimeSpan.FromMinutes(value.Value); } } #endregion // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] public readonly Bindable EndDate = new Bindable(); // Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930) [JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)] private int? maxAttempts { get => MaxAttempts.Value; set => MaxAttempts.Value = value; } public Room() { Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); } /// /// Create a copy of this room without online information. /// Should be used to create a local copy of a room for submitting in the future. /// public Room DeepClone() { var copy = new Room(); copy.CopyFrom(this); copy.RoomID.Value = null; return copy; } public void CopyFrom(Room other) { RoomID.Value = other.RoomID.Value; Name.Value = other.Name.Value; Category.Value = other.Category.Value; if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) Host.Value = other.Host.Value; ChannelId.Value = other.ChannelId.Value; Status.Value = other.Status.Value; Availability.Value = other.Availability.Value; HasPassword.Value = other.HasPassword.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; ParticipantCount.Value = other.ParticipantCount.Value; 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; if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); other.RemoveExpiredPlaylistItems(); if (!Playlist.SequenceEqual(other.Playlist)) { Playlist.Clear(); Playlist.AddRange(other.Playlist); } if (!RecentParticipants.SequenceEqual(other.RecentParticipants)) { RecentParticipants.Clear(); RecentParticipants.AddRange(other.RecentParticipants); } } public void RemoveExpiredPlaylistItems() { // Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended, // and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room. // More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room. if (!(Status.Value is RoomStatusEnded)) Playlist.RemoveAll(i => i.Expired); } #region Newtonsoft.Json implicit ShouldSerialize() methods // 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. [UsedImplicitly] public bool ShouldSerializeHost() => false; #endregion [JsonObject(MemberSerialization.OptIn)] public class RoomPlaylistItemStats { [JsonProperty("count_active")] public int CountActive; [JsonProperty("count_total")] public int CountTotal; [JsonProperty("ruleset_ids")] public int[] RulesetIDs; } [JsonObject(MemberSerialization.OptIn)] public class RoomDifficultyRange { [JsonProperty("min")] public double Min; [JsonProperty("max")] public double Max; } } }