From cb823f367f92fdeaf4db1a7ea3f75777f5a787bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Dec 2023 18:16:45 +0100 Subject: [PATCH 1/3] Simplify `UserActivity` for serialisability over the wire Up until now, the `UserActivity` class hierarchy contained things like beatmap info, room info, full replay info, etc. While this was convenient, it is soon going to be less so, as the data is sent over the wire to the spectator server so that the user's activity can be broadcast to other clients. To counteract this without creating a second separate and slimmed-down class hierarchy, slim down the `UserActivity` structure to contain the bare minimum amounts of data such that the structures aren't overly large and complex to serialise, but also contain enough data that they can be used by receiving clients directly without having to do beatmap or score lookups. --- osu.Desktop/DiscordRichPresence.cs | 36 +-- osu.Game/Online/Chat/NowPlayingCommand.cs | 21 +- osu.Game/Online/SignalRWorkaroundTypes.cs | 16 ++ osu.Game/Users/UserActivity.cs | 276 +++++++++++++++------- 4 files changed, 227 insertions(+), 122 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index caf0a1d9fd..c66725e3e3 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Online.API; @@ -95,17 +94,18 @@ namespace osu.Desktop if (status.Value is UserStatusOnline && activity.Value != null) { - presence.State = truncate(activity.Value.GetStatus(privacyMode.Value == DiscordRichPresenceMode.Limited)); - presence.Details = truncate(getDetails(activity.Value)); + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited; + presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); + presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0) + if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) { presence.Buttons = new[] { new Button { Label = "View beatmap", - Url = $@"{api.WebsiteRootUrl}/beatmapsets/{beatmap.BeatmapSet?.OnlineID}#{ruleset.Value.ShortName}/{beatmap.OnlineID}" + Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } @@ -159,40 +159,20 @@ namespace osu.Desktop }); } - private IBeatmapInfo? getBeatmap(UserActivity activity) + private int? getBeatmapID(UserActivity activity) { switch (activity) { case UserActivity.InGame game: - return game.BeatmapInfo; + return game.BeatmapID; case UserActivity.EditingBeatmap edit: - return edit.BeatmapInfo; + return edit.BeatmapID; } return null; } - private string getDetails(UserActivity activity) - { - switch (activity) - { - case UserActivity.InGame game: - return game.BeatmapInfo.ToString() ?? string.Empty; - - case UserActivity.EditingBeatmap edit: - return edit.BeatmapInfo.ToString() ?? string.Empty; - - case UserActivity.WatchingReplay watching: - return watching.BeatmapInfo?.ToString() ?? string.Empty; - - case UserActivity.InLobby lobby: - return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; - } - - return string.Empty; - } - protected override void Dispose(bool isDisposing) { client.Dispose(); diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index e7018d6993..0e6f6f0bf6 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -7,7 +7,6 @@ using System.Text; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets; @@ -33,9 +32,6 @@ namespace osu.Game.Online.Chat [Resolved] private IBindable currentRuleset { get; set; } = null!; - [Resolved] - private LocalisationManager localisation { get; set; } = null!; - private readonly Channel? target; /// @@ -52,23 +48,28 @@ namespace osu.Game.Online.Chat base.LoadComplete(); string verb; - IBeatmapInfo beatmapInfo; + + int beatmapOnlineID; + string beatmapDisplayTitle; switch (api.Activity.Value) { case UserActivity.InGame game: verb = "playing"; - beatmapInfo = game.BeatmapInfo; + beatmapOnlineID = game.BeatmapID; + beatmapDisplayTitle = game.BeatmapDisplayTitle; break; case UserActivity.EditingBeatmap edit: verb = "editing"; - beatmapInfo = edit.BeatmapInfo; + beatmapOnlineID = edit.BeatmapID; + beatmapDisplayTitle = edit.BeatmapDisplayTitle; break; default: verb = "listening to"; - beatmapInfo = currentBeatmap.Value.BeatmapInfo; + beatmapOnlineID = currentBeatmap.Value.BeatmapInfo.OnlineID; + beatmapDisplayTitle = currentBeatmap.Value.BeatmapInfo.GetDisplayTitle(); break; } @@ -86,9 +87,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - string beatmapInfoString = localisation.GetLocalisedBindableString(beatmapInfo.GetDisplayTitleRomanisable()).Value; - - return beatmapInfo.OnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineID} {beatmapInfoString}]" : beatmapInfoString; + return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 0e3eb0aab0..59a12b3bf1 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Users; namespace osu.Game.Online { @@ -18,6 +19,7 @@ namespace osu.Game.Online { internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[] { + // multiplayer (typeof(ChangeTeamRequest), typeof(MatchUserRequest)), (typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)), (typeof(StopCountdownRequest), typeof(MatchUserRequest)), @@ -28,6 +30,20 @@ namespace osu.Game.Online (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)), (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown)), (typeof(ServerShuttingDownCountdown), typeof(MultiplayerCountdown)), + + // metadata + (typeof(UserActivity.ChoosingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.InSoloGame), typeof(UserActivity)), + (typeof(UserActivity.WatchingReplay), typeof(UserActivity)), + (typeof(UserActivity.SpectatingUser), typeof(UserActivity)), + (typeof(UserActivity.SearchingForLobby), typeof(UserActivity)), + (typeof(UserActivity.InLobby), typeof(UserActivity)), + (typeof(UserActivity.InMultiplayerGame), typeof(UserActivity)), + (typeof(UserActivity.SpectatingMultiplayerGame), typeof(UserActivity)), + (typeof(UserActivity.InPlaylistGame), typeof(UserActivity)), + (typeof(UserActivity.EditingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.ModdingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.TestingBeatmap), typeof(UserActivity)), }; } } diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index c82f642fdc..1b09666df6 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -1,8 +1,11 @@ // 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 MessagePack; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -10,43 +13,84 @@ using osuTK.Graphics; namespace osu.Game.Users { + /// + /// Base class for all structures describing the user's current activity. + /// + /// + /// Warning: keep specs consistent with + /// . + /// + [Serializable] + [MessagePackObject] + [Union(11, typeof(ChoosingBeatmap))] + [Union(12, typeof(InSoloGame))] + [Union(13, typeof(WatchingReplay))] + [Union(14, typeof(SpectatingUser))] + [Union(21, typeof(SearchingForLobby))] + [Union(22, typeof(InLobby))] + [Union(23, typeof(InMultiplayerGame))] + [Union(24, typeof(SpectatingMultiplayerGame))] + [Union(31, typeof(InPlaylistGame))] + [Union(41, typeof(EditingBeatmap))] + [Union(42, typeof(ModdingBeatmap))] + [Union(43, typeof(TestingBeatmap))] public abstract class UserActivity { public abstract string GetStatus(bool hideIdentifiableInformation = false); + public virtual string? GetDetails(bool hideIdentifiableInformation = false) => null; public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; - public class ModdingBeatmap : EditingBeatmap - { - public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; - - public ModdingBeatmap(IBeatmapInfo info) - : base(info) - { - } - } - + [MessagePackObject] public class ChoosingBeatmap : UserActivity { public override string GetStatus(bool hideIdentifiableInformation = false) => "Choosing a beatmap"; } + [MessagePackObject] public abstract class InGame : UserActivity { - public IBeatmapInfo BeatmapInfo { get; } + [Key(0)] + public int BeatmapID { get; set; } - public IRulesetInfo Ruleset { get; } + [Key(1)] + public string BeatmapDisplayTitle { get; set; } = string.Empty; + + [Key(2)] + public int RulesetID { get; set; } + + [Key(3)] + public string RulesetPlayingVerb { get; set; } = string.Empty; // TODO: i'm going with this for now, but this is wasteful protected InGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) { - BeatmapInfo = beatmapInfo; - Ruleset = ruleset; + BeatmapID = beatmapInfo.OnlineID; + BeatmapDisplayTitle = beatmapInfo.GetDisplayTitle(); + + RulesetID = ruleset.OnlineID; + RulesetPlayingVerb = ruleset.CreateInstance().PlayingVerb; } - public override string GetStatus(bool hideIdentifiableInformation = false) => Ruleset.CreateInstance().PlayingVerb; + [SerializationConstructor] + protected InGame() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb; + public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; } + [MessagePackObject] + public class InSoloGame : InGame + { + public InSoloGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public InSoloGame() { } + } + + [MessagePackObject] public class InMultiplayerGame : InGame { public InMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) @@ -54,9 +98,122 @@ namespace osu.Game.Users { } + [SerializationConstructor] + public InMultiplayerGame() + { + } + public override string GetStatus(bool hideIdentifiableInformation = false) => $@"{base.GetStatus(hideIdentifiableInformation)} with others"; } + [MessagePackObject] + public class InPlaylistGame : InGame + { + public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public InPlaylistGame() { } + } + + [MessagePackObject] + public class TestingBeatmap : InGame + { + public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public TestingBeatmap() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap"; + } + + [MessagePackObject] + public class EditingBeatmap : UserActivity + { + [Key(0)] + public int BeatmapID { get; set; } + + [Key(1)] + public string BeatmapDisplayTitle { get; set; } = string.Empty; + + public EditingBeatmap(IBeatmapInfo info) + { + BeatmapID = info.OnlineID; + BeatmapDisplayTitle = info.GetDisplayTitle(); + } + + [SerializationConstructor] + public EditingBeatmap() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap"; + public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; + } + + [MessagePackObject] + public class ModdingBeatmap : EditingBeatmap + { + public ModdingBeatmap(IBeatmapInfo info) + : base(info) + { + } + + [SerializationConstructor] + public ModdingBeatmap() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark; + } + + [MessagePackObject] + public class WatchingReplay : UserActivity + { + [Key(0)] + public long ScoreID { get; set; } + + [Key(1)] + public string PlayerName { get; set; } = string.Empty; + + [Key(2)] + public int BeatmapID { get; set; } + + [Key(3)] + public string? BeatmapDisplayTitle { get; set; } + + public WatchingReplay(ScoreInfo score) + { + ScoreID = score.OnlineID; + PlayerName = score.User.Username; + BeatmapID = score.BeatmapInfo?.OnlineID ?? -1; + BeatmapDisplayTitle = score.BeatmapInfo?.GetDisplayTitle(); + } + + [SerializationConstructor] + public WatchingReplay() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {PlayerName}'s replay"; + public override string? GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; + } + + [MessagePackObject] + public class SpectatingUser : WatchingReplay + { + public SpectatingUser(ScoreInfo score) + : base(score) + { + } + + [SerializationConstructor] + public SpectatingUser() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {PlayerName}"; + } + + [MessagePackObject] public class SpectatingMultiplayerGame : InGame { public SpectatingMultiplayerGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) @@ -64,88 +221,41 @@ namespace osu.Game.Users { } + [SerializationConstructor] + public SpectatingMultiplayerGame() { } + public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}"; } - public class InPlaylistGame : InGame - { - public InPlaylistGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) - : base(beatmapInfo, ruleset) - { - } - } - - public class InSoloGame : InGame - { - public InSoloGame(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) - : base(beatmapInfo, ruleset) - { - } - } - - public class TestingBeatmap : InGame - { - public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap"; - - public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) - : base(beatmapInfo, ruleset) - { - } - } - - public class EditingBeatmap : UserActivity - { - public IBeatmapInfo BeatmapInfo { get; } - - public EditingBeatmap(IBeatmapInfo info) - { - BeatmapInfo = info; - } - - public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap"; - } - - public class WatchingReplay : UserActivity - { - private readonly ScoreInfo score; - - protected string Username => score.User.Username; - - public BeatmapInfo? BeatmapInfo => score.BeatmapInfo; - - public WatchingReplay(ScoreInfo score) - { - this.score = score; - } - - public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {Username}'s replay"; - } - - public class SpectatingUser : WatchingReplay - { - public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {Username}"; - - public SpectatingUser(ScoreInfo score) - : base(score) - { - } - } - + [MessagePackObject] public class SearchingForLobby : UserActivity { public override string GetStatus(bool hideIdentifiableInformation = false) => @"Looking for a lobby"; } + [MessagePackObject] public class InLobby : UserActivity { - public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby"; + [Key(0)] + public long RoomID { get; set; } - public readonly Room Room; + [Key(1)] + public string RoomName { get; set; } = string.Empty; public InLobby(Room room) { - Room = room; + RoomID = room.RoomID.Value ?? -1; + RoomName = room.Name.Value; } + + [SerializationConstructor] + public InLobby() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby"; + + public override string? GetDetails(bool hideIdentifiableInformation = false) => hideIdentifiableInformation + ? null + : RoomName; } } } From d66fa093205320b3a09a20ded2632b68dbf3fe21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Dec 2023 18:21:44 +0100 Subject: [PATCH 2/3] Simplify `UserStatus` to be an enumeration type There were absolutely no gains from having it be a reference type / class, only complications, especially when coming from the serialisation angle. --- osu.Desktop/DiscordRichPresence.cs | 6 +-- .../Online/TestSceneUserClickableAvatar.cs | 2 +- .../Visual/Online/TestSceneUserPanel.cs | 18 ++++---- osu.Game/Online/API/APIAccess.cs | 2 +- .../Online/API/Requests/Responses/APIUser.cs | 2 +- osu.Game/Overlays/Login/LoginPanel.cs | 6 +-- osu.Game/Users/ExtendedUserPanel.cs | 17 +++---- osu.Game/Users/UserStatus.cs | 46 +++++++++++-------- 8 files changed, 53 insertions(+), 46 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index c66725e3e3..f990fd55fc 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -33,7 +33,7 @@ namespace osu.Desktop [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly IBindable status = new Bindable(); + private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); private readonly Bindable privacyMode = new Bindable(); @@ -86,13 +86,13 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value is UserStatusOffline || privacyMode.Value == DiscordRichPresenceMode.Off) + if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; } - if (status.Value is UserStatusOnline && activity.Value != null) + if (status.Value == UserStatus.Online && activity.Value != null) { bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited; presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 9edaa841b2..4539eae25f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online Colour = color ?? "000000", Status = { - Value = new UserStatusOnline() + Value = UserStatus.Online }, }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index c61b572d8c..b3b8fd78d3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online public partial class TestSceneUserPanel : OsuTestScene { private readonly Bindable activity = new Bindable(); - private readonly Bindable status = new Bindable(); + private readonly Bindable status = new Bindable(); private UserGridPanel boundPanel1; private TestUserListPanel boundPanel2; @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3103765, CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - Status = { Value = new UserStatusOnline() } + Status = { Value = UserStatus.Online } }) { Width = 300 }, boundPanel1 = new UserGridPanel(new APIUser { @@ -99,16 +99,16 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestUserStatus() { - AddStep("online", () => status.Value = new UserStatusOnline()); - AddStep("do not disturb", () => status.Value = new UserStatusDoNotDisturb()); - AddStep("offline", () => status.Value = new UserStatusOffline()); + AddStep("online", () => status.Value = UserStatus.Online); + AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb); + AddStep("offline", () => status.Value = UserStatus.Offline); AddStep("null status", () => status.Value = null); } [Test] public void TestUserActivity() { - AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = UserStatus.Online); AddStep("idle", () => activity.Value = null); AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats"))); @@ -127,12 +127,12 @@ namespace osu.Game.Tests.Visual.Online public void TestUserActivityChange() { AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = UserStatus.Online); AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("set offline status", () => status.Value = new UserStatusOffline()); + AddStep("set offline status", () => status.Value = UserStatus.Offline); AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = UserStatus.Online); AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4f586c8fff..21107d61fc 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -247,7 +247,7 @@ namespace osu.Game.Online.API userReq.Success += user => { // todo: save/pull from settings - user.Status.Value = new UserStatusOnline(); + user.Status.Value = UserStatus.Online; setLocalUser(user); diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 2ee66453cf..56eec19fa1 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -43,7 +43,7 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } - public readonly Bindable Status = new Bindable(); + public readonly Bindable Status = new Bindable(); public readonly Bindable Activity = new Bindable(); diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 71ecf2e75a..19af95459f 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -148,17 +148,17 @@ namespace osu.Game.Overlays.Login switch (action.NewValue) { case UserAction.Online: - api.LocalUser.Value.Status.Value = new UserStatusOnline(); + api.LocalUser.Value.Status.Value = UserStatus.Online; dropdown.StatusColour = colours.Green; break; case UserAction.DoNotDisturb: - api.LocalUser.Value.Status.Value = new UserStatusDoNotDisturb(); + api.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb; dropdown.StatusColour = colours.Red; break; case UserAction.AppearOffline: - api.LocalUser.Value.Status.Value = new UserStatusOffline(); + api.LocalUser.Value.Status.Value = UserStatus.Offline; dropdown.StatusColour = colours.Gray7; break; diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index 3c1b68f9ef..18fe852556 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -6,6 +6,7 @@ using osuTK; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -18,7 +19,7 @@ namespace osu.Game.Users { public abstract partial class ExtendedUserPanel : UserPanel { - public readonly Bindable Status = new Bindable(); + public readonly Bindable Status = new Bindable(); public readonly IBindable Activity = new Bindable(); @@ -97,14 +98,14 @@ namespace osu.Game.Users return statusContainer; } - private void displayStatus(UserStatus status, UserActivity activity = null) + private void displayStatus(UserStatus? status, UserActivity activity = null) { if (status != null) { - LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + LastVisitMessage.FadeTo(status == UserStatus.Offline && User.LastVisit.HasValue ? 1 : 0); // Set status message based on activity (if we have one) and status is not offline - if (activity != null && !(status is UserStatusOffline)) + if (activity != null && status != UserStatus.Offline) { statusMessage.Text = activity.GetStatus(); statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); @@ -112,8 +113,8 @@ namespace osu.Game.Users } // Otherwise use only status - statusMessage.Text = status.Message; - statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint); + statusMessage.Text = status.GetLocalisableDescription(); + statusIcon.FadeColour(status.Value.GetAppropriateColour(Colours), 500, Easing.OutQuint); return; } @@ -121,11 +122,11 @@ namespace osu.Game.Users // Fallback to web status if local one is null if (User.IsOnline) { - Status.Value = new UserStatusOnline(); + Status.Value = UserStatus.Online; return; } - Status.Value = new UserStatusOffline(); + Status.Value = UserStatus.Offline; } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index ffd86b78c7..cd25add4d1 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -1,6 +1,8 @@ // 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.ComponentModel; using osu.Framework.Localisation; using osuTK.Graphics; using osu.Game.Graphics; @@ -8,32 +10,36 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Users { - public abstract class UserStatus + public enum UserStatus { - public abstract LocalisableString Message { get; } - public abstract Color4 GetAppropriateColour(OsuColour colours); + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOffline))] + Offline, + + [Description("Do not disturb")] + DoNotDisturb, + + [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))] + Online, } - public class UserStatusOnline : UserStatus + public static class UserStatusExtensions { - public override LocalisableString Message => UsersStrings.StatusOnline; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; - } + public static Color4 GetAppropriateColour(this UserStatus userStatus, OsuColour colours) + { + switch (userStatus) + { + case UserStatus.Offline: + return Color4.Black; - public abstract class UserStatusBusy : UserStatusOnline - { - public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDark; - } + case UserStatus.DoNotDisturb: + return colours.RedDark; - public class UserStatusOffline : UserStatus - { - public override LocalisableString Message => UsersStrings.StatusOffline; - public override Color4 GetAppropriateColour(OsuColour colours) => Color4.Black; - } + case UserStatus.Online: + return colours.GreenDark; - public class UserStatusDoNotDisturb : UserStatus - { - public override LocalisableString Message => "Do not disturb"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.RedDark; + default: + throw new ArgumentOutOfRangeException(nameof(userStatus), userStatus, "Unsupported user status"); + } + } } } From 602550b9c233280d81cb65f8b39d1167a2ccaeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Dec 2023 19:31:42 +0100 Subject: [PATCH 3/3] Fix test failures --- osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index fb36580a42..1e9b0317fb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -11,8 +11,8 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new RulesetInfo())); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestModPresence() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new RulesetInfo())); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() });