// 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.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; namespace osu.Game.Screens.Multi.Components { public abstract class RoomManager : CompositeDrawable, IRoomManager { public event Action RoomsUpdated; private readonly BindableList rooms = new BindableList(); public IBindable InitialRoomsReceived => initialRoomsReceived; private readonly Bindable initialRoomsReceived = new Bindable(); public IBindableList Rooms => rooms; protected IBindable JoinedRoom => joinedRoom; private readonly Bindable joinedRoom = new Bindable(); [Resolved] private RulesetStore rulesets { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } [Resolved] private IAPIProvider api { get; set; } protected RoomManager() { RelativeSizeAxes = Axes.Both; InternalChildren = CreatePollingComponents().Select(p => { p.RoomsReceived = onRoomsReceived; return p; }).ToList(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); PartRoom(); } public virtual void CreateRoom(Room room, Action onSuccess = null, Action onError = null) { room.Host.Value = api.LocalUser.Value; var req = new CreateRoomRequest(room); req.Success += result => { joinedRoom.Value = room; update(room, result); addRoom(room); RoomsUpdated?.Invoke(); onSuccess?.Invoke(room); }; req.Failure += exception => { onError?.Invoke(req.Result?.Error ?? exception.Message); }; api.Queue(req); } private JoinRoomRequest currentJoinRoomRequest; public virtual void JoinRoom(Room room, Action onSuccess = null, Action onError = null) { currentJoinRoomRequest?.Cancel(); currentJoinRoomRequest = new JoinRoomRequest(room); currentJoinRoomRequest.Success += () => { joinedRoom.Value = room; onSuccess?.Invoke(room); }; currentJoinRoomRequest.Failure += exception => { if (!(exception is OperationCanceledException)) Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); onError?.Invoke(exception.ToString()); }; api.Queue(currentJoinRoomRequest); } public virtual void PartRoom() { currentJoinRoomRequest?.Cancel(); if (JoinedRoom.Value == null) return; api.Queue(new PartRoomRequest(joinedRoom.Value)); joinedRoom.Value = null; } private readonly HashSet ignoredRooms = new HashSet(); private void onRoomsReceived(List received) { if (received == null) { ClearRooms(); return; } // Remove past matches foreach (var r in rooms.ToList()) { if (received.All(e => e.RoomID.Value != r.RoomID.Value)) rooms.Remove(r); } for (int i = 0; i < received.Count; i++) { var room = received[i]; Debug.Assert(room.RoomID.Value != null); if (ignoredRooms.Contains(room.RoomID.Value.Value)) continue; room.Position.Value = i; try { update(room, room); addRoom(room); } catch (Exception ex) { Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); ignoredRooms.Add(room.RoomID.Value.Value); rooms.Remove(room); } } RoomsUpdated?.Invoke(); initialRoomsReceived.Value = true; } protected void RemoveRoom(Room room) => rooms.Remove(room); protected void ClearRooms() { rooms.Clear(); initialRoomsReceived.Value = false; } /// /// Updates a local with a remote copy. /// /// The local to update. /// The remote to update with. private void update(Room local, Room remote) { foreach (var pi in remote.Playlist) pi.MapObjects(beatmaps, rulesets); local.CopyFrom(remote); } /// /// Adds a to the list of available rooms. /// /// The to add. private void addRoom(Room room) { var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); if (existing == null) rooms.Add(room); else existing.CopyFrom(room); } protected abstract IEnumerable CreatePollingComponents(); } }