// 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 duration for which the room will be open. Will be null after the room is created. /// /// /// To check the room end time, use . /// public TimeSpan? Duration { get => duration == null ? null : TimeSpan.FromMinutes(duration.Value); set => SetField(ref duration, value == null ? null : (int)value.Value.TotalMinutes); } /// /// The date at which the room was opened. Will be null while the room has not yet been created. /// public DateTimeOffset? StartDate { get => startDate; set => SetField(ref startDate, value); } /// /// The date at which the room will be closed. /// /// /// To set the room duration, use . /// public DateTimeOffset? EndDate { get => endDate; set => SetField(ref endDate, 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 maximum number of attempts on the playlist. Only valid for playlist rooms. /// public int? MaxAttempts { get => maxAttempts; set => SetField(ref maxAttempts, value); } /// /// Describes the items in the playlist. /// public RoomPlaylistItemStats? PlaylistItemStats { get => playlistItemStats; set => SetField(ref playlistItemStats, value); } /// /// Describes the range of difficulty of the room. /// public RoomDifficultyRange? DifficultyRange { get => difficultyRange; set => SetField(ref difficultyRange, 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); } /// /// Provides some extra scoring statistics for the local user in the room. /// public PlaylistAggregateScore? UserScore { get => userScore; set => SetField(ref userScore, value); } /// /// 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 chat channel id for the room. Will be 0 while the room has not yet been created. /// public int ChannelId { get => channelId; private set => SetField(ref channelId, 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; [JsonProperty("duration")] private int? duration; [JsonProperty("starts_at")] private DateTimeOffset? startDate; [JsonProperty("ends_at")] private DateTimeOffset? endDate; // Not yet serialised (not implemented). private int? maxParticipants; [JsonProperty("participant_count")] private int participantCount; [JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)] private int? maxAttempts; [JsonProperty("playlist_item_stats")] private RoomPlaylistItemStats? playlistItemStats; [JsonProperty("difficulty_range")] private RoomDifficultyRange? difficultyRange; [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_user_score")] private PlaylistAggregateScore? userScore; [JsonProperty("current_playlist_item")] private PlaylistItem? currentPlaylistItem; [JsonProperty("channel_id")] private int channelId; // 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("recent_participants")] public readonly BindableList RecentParticipants = new BindableList(); /// /// 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 = other.ChannelId; Status = other.Status; Availability = other.Availability; HasPassword = other.HasPassword; Type = other.Type; MaxParticipants = other.MaxParticipants; ParticipantCount = other.ParticipantCount; EndDate = other.EndDate; UserScore = other.UserScore; QueueMode = other.QueueMode; AutoStartDuration = other.AutoStartDuration; DifficultyRange = other.DifficultyRange; PlaylistItemStats = other.PlaylistItemStats; 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; } } }