// 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.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; 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; namespace osu.Game.Online.Rooms { [JsonObject(MemberSerialization.OptIn)] public partial class Room : IDependencyInjectionCandidate, INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; /// /// The online room ID. Will be null while the room has not yet been created. /// public long? RoomID { get => roomId; set => SetField(ref roomId, value); } /// /// The room name. /// public string Name { get => name; set => SetField(ref name, value); } /// /// Sets the room password. Will be null after the room is created. /// /// /// To check if the room has a password, use . /// public string? Password { get => password; set { SetField(ref password, value); HasPassword = !string.IsNullOrEmpty(value); } } /// /// Whether the room has a password. /// /// /// To set a password, use . /// [JsonProperty("has_password")] public bool HasPassword { get => hasPassword; private set => SetField(ref hasPassword, value); } /// /// The room host. Will be null while the room has not yet been created. /// public APIUser? Host { get => host; set => SetField(ref host, value); } /// /// The room category. /// public RoomCategory Category { get => category; set => SetField(ref category, value); } /// /// The maximum number of users allowed in the room. /// public int? MaxParticipants { get => maxParticipants; set => SetField(ref maxParticipants, value); } /// /// The current number of users in the room. /// public int ParticipantCount { get => participantCount; set => SetField(ref participantCount, value); } /// /// The match type. /// public MatchType Type { get => type; set => SetField(ref type, value); } /// /// The playlist queueing mode. Only valid for multiplayer rooms. /// public QueueMode QueueMode { get => queueMode; set => SetField(ref queueMode, value); } /// /// Whether to automatically skip map intros. Only valid for multiplayer rooms. /// public bool AutoSkip { get => autoSkip; set => SetField(ref autoSkip, value); } /// /// The amount of time before the match is automatically started. Only valid for multiplayer rooms. /// public TimeSpan AutoStartDuration { get => TimeSpan.FromSeconds(autoStartDuration); set => SetField(ref autoStartDuration, (ushort)value.TotalSeconds); } /// /// Represents the current item selected within the room. /// /// /// Only valid for room listing requests (i.e. in the lounge screen), and may not be valid while inside the room. /// public PlaylistItem? CurrentPlaylistItem { get => currentPlaylistItem; set => SetField(ref currentPlaylistItem, value); } /// /// The current room status. /// public RoomStatus Status { get => status; set => SetField(ref status, value); } /// /// Describes which players are able to join the room. /// public RoomAvailability Availability { get => availability; set => SetField(ref availability, value); } [JsonProperty("id")] private long? roomId; [JsonProperty("name")] private string name = string.Empty; [JsonProperty("password")] private string? password; // Not serialised (internal use only). private bool hasPassword; [JsonProperty("host")] private APIUser? host; [JsonProperty("category")] [JsonConverter(typeof(SnakeCaseStringEnumConverter))] private RoomCategory category; // Not yet serialised (not implemented). private int? maxParticipants; [JsonProperty("participant_count")] private int participantCount; [JsonConverter(typeof(SnakeCaseStringEnumConverter))] [JsonProperty("type")] private MatchType type; [JsonConverter(typeof(SnakeCaseStringEnumConverter))] [JsonProperty("queue_mode")] private QueueMode queueMode; [JsonProperty("auto_skip")] private bool autoSkip; [JsonProperty("auto_start_duration")] private ushort autoStartDuration; [JsonProperty("current_playlist_item")] private PlaylistItem? currentPlaylistItem; // Not serialised (see: GetRoomsRequest). private RoomStatus status = new RoomStatusOpen(); // Not yet serialised (not implemented). private RoomAvailability availability; [Cached] [JsonProperty("playlist")] public readonly BindableList Playlist = new BindableList(); [Cached] [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); [JsonProperty("playlist_item_stats")] [Cached] public readonly Bindable PlaylistItemStats = new Bindable(); [JsonProperty("difficulty_range")] [Cached] public readonly Bindable DifficultyRange = new Bindable(); [Cached] public readonly Bindable MaxAttempts = new Bindable(); [Cached] [JsonProperty("current_user_score")] public readonly Bindable UserScore = new Bindable(); [Cached] [JsonProperty("recent_participants")] public readonly BindableList RecentParticipants = new BindableList(); #region Properties only used for room creation request [Cached] 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("starts_at")] public readonly Bindable StartDate = new Bindable(); // 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; } /// /// Copies values from another into this one. /// /// /// **Beware**: This will store references between s. /// /// The to copy values from. public void CopyFrom(Room other) { RoomID = other.RoomID; Name = other.Name; Category = other.Category; if (other.Host != null && Host?.Id != other.Host.Id) Host = other.Host; ChannelId.Value = other.ChannelId.Value; Status = other.Status; Availability = other.Availability; HasPassword = other.HasPassword; Type = other.Type; MaxParticipants = other.MaxParticipants; ParticipantCount = other.ParticipantCount; EndDate.Value = other.EndDate.Value; UserScore.Value = other.UserScore.Value; QueueMode = other.QueueMode; AutoStartDuration = other.AutoStartDuration; DifficultyRange.Value = other.DifficultyRange.Value; PlaylistItemStats.Value = other.PlaylistItemStats.Value; CurrentPlaylistItem = other.CurrentPlaylistItem; AutoSkip = other.AutoSkip; 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 is not RoomStatusEnded) Playlist.RemoveAll(i => i.Expired); } [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; } protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null!) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } } }