// 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 System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Users; namespace osu.Game.Online.Metadata { public abstract partial class MetadataClient : Component, IMetadataClient, IMetadataServer { public abstract IBindable IsConnected { get; } [Resolved] private IAPIProvider api { get; set; } = null!; #region Beatmap metadata updates public abstract Task GetChangesSince(int queueId); public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates); public event Action? ChangedBeatmapSetsArrived; protected Task ProcessChanges(int[] beatmapSetIDs) { ChangedBeatmapSetsArrived?.Invoke(beatmapSetIDs.Distinct().ToArray()); return Task.CompletedTask; } #endregion #region User presence updates /// /// The information about the current user. /// public abstract UserPresence LocalUserPresence { get; } /// /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. /// public abstract IBindableDictionary UserPresences { get; } /// /// Dictionary keyed by user ID containing all of the information about currently online friends received from the server. /// public abstract IBindableDictionary FriendPresences { get; } /// /// Attempts to retrieve the presence of a user. /// /// The user ID. /// The user presence, or null if not available or the user's offline. public UserPresence? GetPresence(int userId) { if (userId == api.LocalUser.Value.OnlineID) return LocalUserPresence; if (FriendPresences.TryGetValue(userId, out UserPresence presence)) return presence; if (UserPresences.TryGetValue(userId, out presence)) return presence; return null; } public abstract Task UpdateActivity(UserActivity? activity); public abstract Task UpdateStatus(UserStatus? status); private int userPresenceWatchCount; protected bool IsWatchingUserPresence => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; /// /// Signals to the server that we want to begin receiving status updates for all users. /// /// An which will end the session when disposed. public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this); Task IMetadataServer.BeginWatchingUserPresence() { if (Interlocked.Increment(ref userPresenceWatchCount) == 1) return BeginWatchingUserPresenceInternal(); return Task.CompletedTask; } Task IMetadataServer.EndWatchingUserPresence() { if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) return EndWatchingUserPresenceInternal(); return Task.CompletedTask; } protected abstract Task BeginWatchingUserPresenceInternal(); protected abstract Task EndWatchingUserPresenceInternal(); public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); private class UserPresenceWatchToken : IDisposable { private readonly IMetadataServer server; private bool isDisposed; public UserPresenceWatchToken(IMetadataServer server) { this.server = server; server.BeginWatchingUserPresence().FireAndForget(); } public void Dispose() { if (isDisposed) return; server.EndWatchingUserPresence().FireAndForget(); isDisposed = true; } } #endregion #region Daily Challenge public abstract IBindable DailyChallengeInfo { get; } public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info); #endregion #region Multiplayer room watching public abstract Task BeginWatchingMultiplayerRoom(long id); public abstract Task EndWatchingMultiplayerRoom(long id); public event Action? MultiplayerRoomScoreSet; Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent) { if (MultiplayerRoomScoreSet != null) Schedule(MultiplayerRoomScoreSet, roomScoreSetEvent); return Task.CompletedTask; } #endregion #region Disconnection handling public event Action? Disconnecting; public virtual Task DisconnectRequested() { Schedule(() => Disconnecting?.Invoke()); return Task.CompletedTask; } #endregion } }