From 6cca3a3dc82cb2b7972c8232d00aa6dcf119be96 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Oct 2022 14:55:24 +0900 Subject: [PATCH 001/152] Add new API requests --- .../Online/API/Requests/ChatAckRequest.cs | 21 +++++++++++ .../API/Requests/GetNotificationsRequest.cs | 12 ++++++ .../API/Requests/Responses/APINotification.cs | 37 +++++++++++++++++++ .../Responses/APINotificationsBundle.cs | 20 ++++++++++ .../API/Requests/Responses/ChatAckResponse.cs | 15 ++++++++ .../API/Requests/Responses/ChatSilence.cs | 17 +++++++++ 6 files changed, 122 insertions(+) create mode 100644 osu.Game/Online/API/Requests/ChatAckRequest.cs create mode 100644 osu.Game/Online/API/Requests/GetNotificationsRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APINotification.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs create mode 100644 osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs create mode 100644 osu.Game/Online/API/Requests/Responses/ChatSilence.cs diff --git a/osu.Game/Online/API/Requests/ChatAckRequest.cs b/osu.Game/Online/API/Requests/ChatAckRequest.cs new file mode 100644 index 0000000000..f09df4908e --- /dev/null +++ b/osu.Game/Online/API/Requests/ChatAckRequest.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class ChatAckRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + return req; + } + + protected override string Target => "chat/ack"; + } +} diff --git a/osu.Game/Online/API/Requests/GetNotificationsRequest.cs b/osu.Game/Online/API/Requests/GetNotificationsRequest.cs new file mode 100644 index 0000000000..e419807a85 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetNotificationsRequest.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetNotificationsRequest : APIRequest + { + protected override string Target => "notifications"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APINotification.cs b/osu.Game/Online/API/Requests/Responses/APINotification.cs new file mode 100644 index 0000000000..e1f0fa7221 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APINotification.cs @@ -0,0 +1,37 @@ +// 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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class APINotification + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = null!; + + [JsonProperty("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + [JsonProperty("object_type")] + public string ObjectType { get; set; } = null!; + + [JsonProperty("object_id")] + public string ObjectId { get; set; } = null!; + + [JsonProperty("source_user_id")] + public long? SourceUserId { get; set; } + + [JsonProperty("is_read")] + public bool IsRead { get; set; } + + [JsonProperty("details")] + public Dictionary? Details { get; set; } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs b/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs new file mode 100644 index 0000000000..067f1066b3 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class APINotificationsBundle + { + [JsonProperty("has_more")] + public bool HasMore { get; set; } + + [JsonProperty("notifications")] + public APINotification[] Notifications { get; set; } = null!; + + [JsonProperty("notification_endpoint")] + public string Endpoint { get; set; } = null!; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs new file mode 100644 index 0000000000..6ed22a19b2 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/ChatAckResponse.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class ChatAckResponse + { + [JsonProperty("silences")] + public List Silences { get; set; } = null!; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/ChatSilence.cs b/osu.Game/Online/API/Requests/Responses/ChatSilence.cs new file mode 100644 index 0000000000..45fd6e1ba3 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/ChatSilence.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class ChatSilence + { + [JsonProperty("id")] + public uint Id { get; set; } + + [JsonProperty("user_id")] + public uint UserId { get; set; } + } +} From 33bb1212d1e84c3d887380d27f6159ca95361ad5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 16:19:15 +0900 Subject: [PATCH 002/152] Add notifications websocket + chat implementation --- osu.Game/Online/Chat/Message.cs | 7 ++ .../Online/Notifications/EndChatRequest.cs | 16 +++ .../Notifications/NewChatMessageData.cs | 29 +++++ .../Notifications/NotificationsClient.cs | 113 ++++++++++++++++++ .../NotificationsClientConnector.cs | 65 ++++++++++ .../NotificationsClient_Processing.cs | 102 ++++++++++++++++ .../Online/Notifications/SocketMessage.cs | 21 ++++ .../Online/Notifications/StartChatRequest.cs | 16 +++ 8 files changed, 369 insertions(+) create mode 100644 osu.Game/Online/Notifications/EndChatRequest.cs create mode 100644 osu.Game/Online/Notifications/NewChatMessageData.cs create mode 100644 osu.Game/Online/Notifications/NotificationsClient.cs create mode 100644 osu.Game/Online/Notifications/NotificationsClientConnector.cs create mode 100644 osu.Game/Online/Notifications/NotificationsClient_Processing.cs create mode 100644 osu.Game/Online/Notifications/SocketMessage.cs create mode 100644 osu.Game/Online/Notifications/StartChatRequest.cs diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 86562341eb..25c5b0853f 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -30,6 +30,13 @@ namespace osu.Game.Online.Chat [JsonProperty(@"sender")] public APIUser Sender; + [JsonProperty(@"sender_id")] + public int SenderId + { + get => Sender?.Id ?? 0; + set => Sender = new APIUser { Id = value }; + } + [JsonConstructor] public Message() { diff --git a/osu.Game/Online/Notifications/EndChatRequest.cs b/osu.Game/Online/Notifications/EndChatRequest.cs new file mode 100644 index 0000000000..1173b1e8d0 --- /dev/null +++ b/osu.Game/Online/Notifications/EndChatRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.Notifications +{ + [JsonObject(MemberSerialization.OptIn)] + public class EndChatRequest : SocketMessage + { + public EndChatRequest() + { + Event = "chat.end"; + } + } +} diff --git a/osu.Game/Online/Notifications/NewChatMessageData.cs b/osu.Game/Online/Notifications/NewChatMessageData.cs new file mode 100644 index 0000000000..b388afa743 --- /dev/null +++ b/osu.Game/Online/Notifications/NewChatMessageData.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.Notifications +{ + [JsonObject(MemberSerialization.OptIn)] + public class NewChatMessageData + { + [JsonProperty("messages")] + public List Messages { get; set; } = null!; + + [JsonProperty("users")] + private List users { get; set; } = null!; + + [OnDeserialized] + private void onDeserialised(StreamingContext context) + { + foreach (var m in Messages) + m.Sender = users.Single(u => u.OnlineID == m.SenderId); + } + } +} diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs new file mode 100644 index 0000000000..63260e5df9 --- /dev/null +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -0,0 +1,113 @@ +// 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.Diagnostics; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Logging; +using osu.Game.Online.API; + +namespace osu.Game.Online.Notifications +{ + public partial class NotificationsClient : SocketClient + { + private readonly ClientWebSocket socket; + private readonly string endpoint; + private readonly IAPIProvider api; + + public NotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api) + { + this.socket = socket; + this.endpoint = endpoint; + this.api = api; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false); + await onConnectedAsync(); + runReadLoop(cancellationToken); + } + + private void runReadLoop(CancellationToken cancellationToken) => Task.Run((Func)(async () => + { + byte[] buffer = new byte[1024]; + StringBuilder messageResult = new StringBuilder(); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken); + + switch (result.MessageType) + { + case WebSocketMessageType.Text: + messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count])); + + if (result.EndOfMessage) + { + SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString()); + messageResult.Clear(); + + Debug.Assert(message != null); + + if (message.Error != null) + { + Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network); + break; + } + + await onMessageReceivedAsync(message); + } + + break; + + case WebSocketMessageType.Binary: + throw new NotImplementedException(); + + case WebSocketMessageType.Close: + throw new Exception("Connection closed by remote host."); + } + } + catch (Exception ex) + { + await InvokeClosed(ex); + return; + } + } + }), cancellationToken); + + private async Task closeAsync() + { + try + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None).ConfigureAwait(false); + } + catch + { + // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway. + } + } + + private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken) + { + if (socket.State != WebSocketState.Open) + return; + + await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken); + } + + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + await closeAsync(); + socket.Dispose(); + } + } +} diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs new file mode 100644 index 0000000000..18b2a1b19d --- /dev/null +++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs @@ -0,0 +1,65 @@ +// 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.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.Notifications +{ + public class NotificationsClientConnector : SocketClientConnector + { + public event Action? ChannelJoined; + public event Action>? NewMessages; + public event Action? PresenceReceived; + + private readonly IAPIProvider api; + private bool chatStarted; + + public NotificationsClientConnector(IAPIProvider api) + : base(api) + { + this.api = api; + } + + public void StartChat() + { + chatStarted = true; + + if (CurrentConnection is NotificationsClient client) + client.EnableChat = true; + } + + protected override async Task BuildConnectionAsync(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + var req = new GetNotificationsRequest(); + req.Success += bundle => tcs.SetResult(bundle.Endpoint); + req.Failure += ex => tcs.SetException(ex); + api.Queue(req); + + string endpoint = await tcs.Task; + + ClientWebSocket socket = new ClientWebSocket(); + socket.Options.SetRequestHeader("Authorization", $"Bearer {api.AccessToken}"); + socket.Options.Proxy = WebRequest.DefaultWebProxy; + if (socket.Options.Proxy != null) + socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; + + return new NotificationsClient(socket, endpoint, api) + { + ChannelJoined = c => ChannelJoined?.Invoke(c), + NewMessages = m => NewMessages?.Invoke(m), + PresenceReceived = () => PresenceReceived?.Invoke(), + EnableChat = chatStarted + }; + } + } +} diff --git a/osu.Game/Online/Notifications/NotificationsClient_Processing.cs b/osu.Game/Online/Notifications/NotificationsClient_Processing.cs new file mode 100644 index 0000000000..4950a53f6f --- /dev/null +++ b/osu.Game/Online/Notifications/NotificationsClient_Processing.cs @@ -0,0 +1,102 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.Notifications +{ + public partial class NotificationsClient + { + public Action? ChannelJoined; + public Action>? NewMessages; + public Action? PresenceReceived; + + private bool enableChat; + private long lastMessageId; + + public bool EnableChat + { + get => enableChat; + set + { + enableChat = value; + Task.Run(startChatIfEnabledAsync); + } + } + + private async Task onConnectedAsync() + { + await startChatIfEnabledAsync(); + } + + private async Task startChatIfEnabledAsync() + { + if (!EnableChat) + return; + + await sendMessage(new StartChatRequest(), CancellationToken.None); + + var fetchReq = new GetUpdatesRequest(lastMessageId); + + fetchReq.Success += updates => + { + if (updates?.Presence != null) + { + foreach (var channel in updates.Presence) + handleJoinedChannel(channel); + + //todo: handle left channels + + handleMessages(updates.Messages); + } + + PresenceReceived?.Invoke(); + }; + + api.Queue(fetchReq); + } + + private Task onMessageReceivedAsync(SocketMessage message) + { + switch (message.Event) + { + case "chat.message.new": + Debug.Assert(message.Data != null); + + NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString()); + Debug.Assert(messageData != null); + + List messages = messageData.Messages.Where(m => m.Sender.OnlineID != api.LocalUser.Value.OnlineID).ToList(); + + foreach (var msg in messages) + handleJoinedChannel(new Channel(msg.Sender) { Id = msg.ChannelId }); + + handleMessages(messages); + break; + } + + return Task.CompletedTask; + } + + private void handleJoinedChannel(Channel channel) + { + // we received this from the server so should mark the channel already joined. + channel.Joined.Value = true; + ChannelJoined?.Invoke(channel); + } + + private void handleMessages(List messages) + { + NewMessages?.Invoke(messages); + lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; + } + } +} diff --git a/osu.Game/Online/Notifications/SocketMessage.cs b/osu.Game/Online/Notifications/SocketMessage.cs new file mode 100644 index 0000000000..6b5f3435fc --- /dev/null +++ b/osu.Game/Online/Notifications/SocketMessage.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.Online.Notifications +{ + [JsonObject(MemberSerialization.OptIn)] + public class SocketMessage + { + [JsonProperty("event")] + public string Event { get; set; } = null!; + + [JsonProperty("data")] + public JObject? Data { get; set; } + + [JsonProperty("error")] + public string? Error { get; set; } + } +} diff --git a/osu.Game/Online/Notifications/StartChatRequest.cs b/osu.Game/Online/Notifications/StartChatRequest.cs new file mode 100644 index 0000000000..274738886d --- /dev/null +++ b/osu.Game/Online/Notifications/StartChatRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.Notifications +{ + [JsonObject(MemberSerialization.OptIn)] + public class StartChatRequest : SocketMessage + { + public StartChatRequest() + { + Event = "chat.start"; + } + } +} From 2f731f86bad8d71790fe0d63e400a6601b5e6408 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 16:22:35 +0900 Subject: [PATCH 003/152] Adjust ChannelManager to use notifications client --- .../Components/TournamentMatchChatDisplay.cs | 2 +- osu.Game/Online/Chat/ChannelManager.cs | 119 ++++++------------ osu.Game/OsuGame.cs | 17 +-- 3 files changed, 43 insertions(+), 95 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 6a8e4aa951..ca2b400e8b 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Components if (manager == null) { - AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } }); + AddInternal(manager = new ChannelManager(api)); Channel.BindTo(manager.CurrentChannel); } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index ec84b0643d..b63f841f59 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -6,16 +6,17 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game.Database; -using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Notifications; using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Online.Chat @@ -23,7 +24,7 @@ namespace osu.Game.Online.Chat /// /// Manages everything channel related /// - public class ChannelManager : PollingComponent, IChannelPostTarget + public class ChannelManager : CompositeComponent, IChannelPostTarget { /// /// The channels the player joins on startup @@ -68,9 +69,12 @@ namespace osu.Game.Online.Chat [Resolved] private UserLookupCache users { get; set; } - public readonly BindableBool HighPollRate = new BindableBool(); + [Resolved] + private NotificationsClientConnector connector { get; set; } - private readonly IBindable isIdle = new BindableBool(); + private readonly IBindable apiState = new Bindable(); + private bool channelsInitialised; + private ScheduledDelegate ackDelegate; public ChannelManager(IAPIProvider api) { @@ -78,30 +82,34 @@ namespace osu.Game.Online.Chat CurrentChannel.ValueChanged += currentChannelChanged; } - [BackgroundDependencyLoader(permitNulls: true)] - private void load(IdleTracker idleTracker) + [BackgroundDependencyLoader] + private void load() { - HighPollRate.BindValueChanged(updatePollRate); - isIdle.BindValueChanged(updatePollRate, true); - - if (idleTracker != null) - isIdle.BindTo(idleTracker.IsIdle); - } - - private void updatePollRate(ValueChangedEvent valueChangedEvent) - { - // Polling will eventually be replaced with websocket, but let's avoid doing these background operations as much as possible for now. - // The only loss will be delayed PM/message highlight notifications. - int millisecondsBetweenPolls = HighPollRate.Value ? 1000 : 60000; - - if (isIdle.Value) - millisecondsBetweenPolls *= 10; - - if (TimeBetweenPolls.Value != millisecondsBetweenPolls) + connector.ChannelJoined += ch => joinChannel(ch); + connector.NewMessages += addMessages; + connector.PresenceReceived += () => { - TimeBetweenPolls.Value = millisecondsBetweenPolls; - Logger.Log($"Chat is now polling every {TimeBetweenPolls.Value} ms"); - } + if (!channelsInitialised) + { + channelsInitialised = true; + // we want this to run after the first presence so we can see if the user is in any channels already. + initializeChannels(); + } + }; + + connector.StartChat(); + + apiState.BindTo(api.State); + apiState.BindValueChanged(status => + { + ackDelegate?.Cancel(); + + if (status.NewValue == APIState.Online) + { + Scheduler.Add(ackDelegate = new ScheduledDelegate(() => api.Queue(new ChatAckRequest()), 0, 60000)); + // Todo: Handle silences. + } + }, true); } /// @@ -328,7 +336,7 @@ namespace osu.Game.Online.Chat } } - private void handleChannelMessages(IEnumerable messages) + private void addMessages(List messages) { var channels = JoinedChannels.ToList(); @@ -376,7 +384,7 @@ namespace osu.Game.Online.Chat var fetchInitialMsgReq = new GetMessagesRequest(channel); fetchInitialMsgReq.Success += messages => { - handleChannelMessages(messages); + addMessages(messages); channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none. }; @@ -464,7 +472,7 @@ namespace osu.Game.Online.Chat { channel.Id = resChannel.ChannelID.Value; - handleChannelMessages(resChannel.RecentMessages); + addMessages(resChannel.RecentMessages); channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none. } }; @@ -574,57 +582,6 @@ namespace osu.Game.Online.Chat } } - private long lastMessageId; - - private bool channelsInitialised; - - protected override Task Poll() - { - if (!api.IsLoggedIn) - return base.Poll(); - - var fetchReq = new GetUpdatesRequest(lastMessageId); - - var tcs = new TaskCompletionSource(); - - fetchReq.Success += updates => - { - if (updates?.Presence != null) - { - foreach (var channel in updates.Presence) - { - // we received this from the server so should mark the channel already joined. - channel.Joined.Value = true; - joinChannel(channel); - } - - //todo: handle left channels - - handleChannelMessages(updates.Messages); - - foreach (var group in updates.Messages.GroupBy(m => m.ChannelId)) - JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); - - lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId; - } - - if (!channelsInitialised) - { - channelsInitialised = true; - // we want this to run after the first presence so we can see if the user is in any channels already. - initializeChannels(); - } - - tcs.SetResult(true); - }; - - fetchReq.Failure += _ => tcs.SetResult(false); - - api.Queue(fetchReq); - - return tcs.Task; - } - /// /// Marks the as read /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2bdcb57f2a..7a42007fb8 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -44,6 +44,7 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Notifications; using osu.Game.Overlays; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; @@ -83,6 +84,7 @@ namespace osu.Game private ChatOverlay chatOverlay; private ChannelManager channelManager; + private NotificationsClientConnector notificationsClient; [NotNull] protected readonly NotificationOverlay Notifications = new NotificationOverlay(); @@ -676,6 +678,7 @@ namespace osu.Game { base.Dispose(isDisposing); SentryLogger.Dispose(); + notificationsClient.Dispose(); } protected override IDictionary GetFrameworkConfigDefaults() @@ -879,6 +882,7 @@ namespace osu.Game loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); + loadComponentSingleFile(notificationsClient = new NotificationsClientConnector(API), AddInternal, true); loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); @@ -908,19 +912,6 @@ namespace osu.Game loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add); - chatOverlay.State.BindValueChanged(_ => updateChatPollRate()); - // Multiplayer modes need to increase poll rate temporarily. - API.Activity.BindValueChanged(_ => updateChatPollRate(), true); - - void updateChatPollRate() - { - channelManager.HighPollRate.Value = - chatOverlay.State.Value == Visibility.Visible - || API.Activity.Value is UserActivity.InLobby - || API.Activity.Value is UserActivity.InMultiplayerGame - || API.Activity.Value is UserActivity.SpectatingMultiplayerGame; - } - Add(difficultyRecommender); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); From efa82569117573894533e22beaadcc20a69579f3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 16:32:17 +0900 Subject: [PATCH 004/152] Use more verbatim strings --- .../API/Requests/GetNotificationsRequest.cs | 2 +- .../API/Requests/Responses/APINotification.cs | 16 ++++++++-------- .../Requests/Responses/APINotificationsBundle.cs | 6 +++--- osu.Game/Online/Notifications/EndChatRequest.cs | 2 +- .../Online/Notifications/NewChatMessageData.cs | 4 ++-- .../Online/Notifications/NotificationsClient.cs | 2 +- .../NotificationsClientConnector.cs | 2 +- .../NotificationsClient_Processing.cs | 5 ++++- osu.Game/Online/Notifications/SocketMessage.cs | 6 +++--- .../Online/Notifications/StartChatRequest.cs | 2 +- 10 files changed, 25 insertions(+), 22 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetNotificationsRequest.cs b/osu.Game/Online/API/Requests/GetNotificationsRequest.cs index e419807a85..afd4da296e 100644 --- a/osu.Game/Online/API/Requests/GetNotificationsRequest.cs +++ b/osu.Game/Online/API/Requests/GetNotificationsRequest.cs @@ -7,6 +7,6 @@ namespace osu.Game.Online.API.Requests { public class GetNotificationsRequest : APIRequest { - protected override string Target => "notifications"; + protected override string Target => @"notifications"; } } diff --git a/osu.Game/Online/API/Requests/Responses/APINotification.cs b/osu.Game/Online/API/Requests/Responses/APINotification.cs index e1f0fa7221..2d9122c04b 100644 --- a/osu.Game/Online/API/Requests/Responses/APINotification.cs +++ b/osu.Game/Online/API/Requests/Responses/APINotification.cs @@ -10,28 +10,28 @@ namespace osu.Game.Online.API.Requests.Responses [JsonObject(MemberSerialization.OptIn)] public class APINotification { - [JsonProperty("id")] + [JsonProperty(@"id")] public long Id { get; set; } - [JsonProperty("name")] + [JsonProperty(@"name")] public string Name { get; set; } = null!; - [JsonProperty("created_at")] + [JsonProperty(@"created_at")] public DateTimeOffset? CreatedAt { get; set; } - [JsonProperty("object_type")] + [JsonProperty(@"object_type")] public string ObjectType { get; set; } = null!; - [JsonProperty("object_id")] + [JsonProperty(@"object_id")] public string ObjectId { get; set; } = null!; - [JsonProperty("source_user_id")] + [JsonProperty(@"source_user_id")] public long? SourceUserId { get; set; } - [JsonProperty("is_read")] + [JsonProperty(@"is_read")] public bool IsRead { get; set; } - [JsonProperty("details")] + [JsonProperty(@"details")] public Dictionary? Details { get; set; } } } diff --git a/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs b/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs index 067f1066b3..ae299e2614 100644 --- a/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs +++ b/osu.Game/Online/API/Requests/Responses/APINotificationsBundle.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests.Responses [JsonObject(MemberSerialization.OptIn)] public class APINotificationsBundle { - [JsonProperty("has_more")] + [JsonProperty(@"has_more")] public bool HasMore { get; set; } - [JsonProperty("notifications")] + [JsonProperty(@"notifications")] public APINotification[] Notifications { get; set; } = null!; - [JsonProperty("notification_endpoint")] + [JsonProperty(@"notification_endpoint")] public string Endpoint { get; set; } = null!; } } diff --git a/osu.Game/Online/Notifications/EndChatRequest.cs b/osu.Game/Online/Notifications/EndChatRequest.cs index 1173b1e8d0..f863511804 100644 --- a/osu.Game/Online/Notifications/EndChatRequest.cs +++ b/osu.Game/Online/Notifications/EndChatRequest.cs @@ -10,7 +10,7 @@ namespace osu.Game.Online.Notifications { public EndChatRequest() { - Event = "chat.end"; + Event = @"chat.end"; } } } diff --git a/osu.Game/Online/Notifications/NewChatMessageData.cs b/osu.Game/Online/Notifications/NewChatMessageData.cs index b388afa743..eda9a3a11b 100644 --- a/osu.Game/Online/Notifications/NewChatMessageData.cs +++ b/osu.Game/Online/Notifications/NewChatMessageData.cs @@ -13,10 +13,10 @@ namespace osu.Game.Online.Notifications [JsonObject(MemberSerialization.OptIn)] public class NewChatMessageData { - [JsonProperty("messages")] + [JsonProperty(@"messages")] public List Messages { get; set; } = null!; - [JsonProperty("users")] + [JsonProperty(@"users")] private List users { get; set; } = null!; [OnDeserialized] diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index 63260e5df9..af9e3ba1e7 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -87,7 +87,7 @@ namespace osu.Game.Online.Notifications { try { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None).ConfigureAwait(false); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false); } catch { diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs index 18b2a1b19d..e938ed59f0 100644 --- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online.Notifications string endpoint = await tcs.Task; ClientWebSocket socket = new ClientWebSocket(); - socket.Options.SetRequestHeader("Authorization", $"Bearer {api.AccessToken}"); + socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); socket.Options.Proxy = WebRequest.DefaultWebProxy; if (socket.Options.Proxy != null) socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; diff --git a/osu.Game/Online/Notifications/NotificationsClient_Processing.cs b/osu.Game/Online/Notifications/NotificationsClient_Processing.cs index 4950a53f6f..35cf737135 100644 --- a/osu.Game/Online/Notifications/NotificationsClient_Processing.cs +++ b/osu.Game/Online/Notifications/NotificationsClient_Processing.cs @@ -27,6 +27,9 @@ namespace osu.Game.Online.Notifications get => enableChat; set { + if (enableChat == value) + return; + enableChat = value; Task.Run(startChatIfEnabledAsync); } @@ -68,7 +71,7 @@ namespace osu.Game.Online.Notifications { switch (message.Event) { - case "chat.message.new": + case @"chat.message.new": Debug.Assert(message.Data != null); NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString()); diff --git a/osu.Game/Online/Notifications/SocketMessage.cs b/osu.Game/Online/Notifications/SocketMessage.cs index 6b5f3435fc..c52072ccd5 100644 --- a/osu.Game/Online/Notifications/SocketMessage.cs +++ b/osu.Game/Online/Notifications/SocketMessage.cs @@ -9,13 +9,13 @@ namespace osu.Game.Online.Notifications [JsonObject(MemberSerialization.OptIn)] public class SocketMessage { - [JsonProperty("event")] + [JsonProperty(@"event")] public string Event { get; set; } = null!; - [JsonProperty("data")] + [JsonProperty(@"data")] public JObject? Data { get; set; } - [JsonProperty("error")] + [JsonProperty(@"error")] public string? Error { get; set; } } } diff --git a/osu.Game/Online/Notifications/StartChatRequest.cs b/osu.Game/Online/Notifications/StartChatRequest.cs index 274738886d..d17644ee3b 100644 --- a/osu.Game/Online/Notifications/StartChatRequest.cs +++ b/osu.Game/Online/Notifications/StartChatRequest.cs @@ -10,7 +10,7 @@ namespace osu.Game.Online.Notifications { public StartChatRequest() { - Event = "chat.start"; + Event = @"chat.start"; } } } From 527b1d9db10e0bdcf4d5c75ac64f4794662bc979 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 17:53:28 +0900 Subject: [PATCH 005/152] Generalise + add polling-style for usage in tests --- osu.Game/Online/HubClient.cs | 2 +- .../Notifications/NotificationsClient.cs | 147 ++++++++--------- .../NotificationsClientConnector.cs | 42 ++--- .../NotificationsClient_Processing.cs | 105 ------------- .../Polling/PollingNotificationsClient.cs | 37 +++++ .../PollingNotificationsClientConnector.cs | 26 +++ .../{ => WebSocket}/EndChatRequest.cs | 5 +- .../{ => WebSocket}/NewChatMessageData.cs | 5 +- .../{ => WebSocket}/SocketMessage.cs | 5 +- .../{ => WebSocket}/StartChatRequest.cs | 5 +- .../WebSocket/WebSocketNotificationsClient.cs | 148 ++++++++++++++++++ .../WebSocketNotificationsClientConnector.cs | 46 ++++++ osu.Game/Online/SocketClient.cs | 2 +- osu.Game/Online/SocketClientConnector.cs | 2 +- osu.Game/OsuGame.cs | 3 +- 15 files changed, 357 insertions(+), 223 deletions(-) delete mode 100644 osu.Game/Online/Notifications/NotificationsClient_Processing.cs create mode 100644 osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs create mode 100644 osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs rename osu.Game/Online/Notifications/{ => WebSocket}/EndChatRequest.cs (65%) rename osu.Game/Online/Notifications/{ => WebSocket}/NewChatMessageData.cs (83%) rename osu.Game/Online/Notifications/{ => WebSocket}/SocketMessage.cs (77%) rename osu.Game/Online/Notifications/{ => WebSocket}/StartChatRequest.cs (66%) create mode 100644 osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs create mode 100644 osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs diff --git a/osu.Game/Online/HubClient.cs b/osu.Game/Online/HubClient.cs index 262e298f34..e80931eeae 100644 --- a/osu.Game/Online/HubClient.cs +++ b/osu.Game/Online/HubClient.cs @@ -17,7 +17,7 @@ namespace osu.Game.Online Connection.Closed += InvokeClosed; } - public override Task StartAsync(CancellationToken cancellationToken) => Connection.StartAsync(cancellationToken); + public override Task ConnectAsync(CancellationToken cancellationToken) => Connection.StartAsync(cancellationToken); public override async ValueTask DisposeAsync() { diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index af9e3ba1e7..6d9226ca17 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -2,112 +2,95 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; -using System.Net.WebSockets; -using System.Text; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Logging; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; namespace osu.Game.Online.Notifications { - public partial class NotificationsClient : SocketClient + /// + /// An abstract client which receives notification-related events (chat/notifications). + /// + public abstract class NotificationsClient : SocketClient { - private readonly ClientWebSocket socket; - private readonly string endpoint; + public Action? ChannelJoined; + public Action>? NewMessages; + public Action? PresenceReceived; + private readonly IAPIProvider api; - public NotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api) + private bool enableChat; + private long lastMessageId; + + protected NotificationsClient(IAPIProvider api) { - this.socket = socket; - this.endpoint = endpoint; this.api = api; } - public override async Task StartAsync(CancellationToken cancellationToken) + public bool EnableChat { - await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false); - await onConnectedAsync(); - runReadLoop(cancellationToken); - } - - private void runReadLoop(CancellationToken cancellationToken) => Task.Run((Func)(async () => - { - byte[] buffer = new byte[1024]; - StringBuilder messageResult = new StringBuilder(); - - while (!cancellationToken.IsCancellationRequested) + get => enableChat; + set { - try - { - WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken); - - switch (result.MessageType) - { - case WebSocketMessageType.Text: - messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count])); - - if (result.EndOfMessage) - { - SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString()); - messageResult.Clear(); - - Debug.Assert(message != null); - - if (message.Error != null) - { - Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network); - break; - } - - await onMessageReceivedAsync(message); - } - - break; - - case WebSocketMessageType.Binary: - throw new NotImplementedException(); - - case WebSocketMessageType.Close: - throw new Exception("Connection closed by remote host."); - } - } - catch (Exception ex) - { - await InvokeClosed(ex); + if (enableChat == value) return; + + enableChat = value; + + if (EnableChat) + Task.Run(StartChatAsync); + } + } + + public override async Task ConnectAsync(CancellationToken cancellationToken) + { + if (EnableChat) + await StartChatAsync(); + } + + protected virtual Task StartChatAsync() + { + api.Queue(CreateFetchMessagesRequest(0)); + return Task.CompletedTask; + } + + protected APIRequest CreateFetchMessagesRequest(long? lastMessageId = null) + { + var fetchReq = new GetUpdatesRequest(lastMessageId ?? this.lastMessageId); + + fetchReq.Success += updates => + { + if (updates?.Presence != null) + { + foreach (var channel in updates.Presence) + HandleJoinedChannel(channel); + + //todo: handle left channels + + HandleMessages(updates.Messages); } - } - }), cancellationToken); - private async Task closeAsync() - { - try - { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false); - } - catch - { - // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway. - } + PresenceReceived?.Invoke(); + }; + + return fetchReq; } - private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken) + protected void HandleJoinedChannel(Channel channel) { - if (socket.State != WebSocketState.Open) - return; - - await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken); + // we received this from the server so should mark the channel already joined. + channel.Joined.Value = true; + ChannelJoined?.Invoke(channel); } - public override async ValueTask DisposeAsync() + protected void HandleMessages(List messages) { - await base.DisposeAsync(); - await closeAsync(); - socket.Dispose(); + NewMessages?.Invoke(messages); + lastMessageId = Math.Max(lastMessageId, messages.LastOrDefault()?.Id ?? 0); } } } diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs index e938ed59f0..5b2d6a4e13 100644 --- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs @@ -3,29 +3,27 @@ using System; using System.Collections.Generic; -using System.Net; -using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; namespace osu.Game.Online.Notifications { - public class NotificationsClientConnector : SocketClientConnector + /// + /// An abstract connector or s. + /// + public abstract class NotificationsClientConnector : SocketClientConnector { public event Action? ChannelJoined; public event Action>? NewMessages; public event Action? PresenceReceived; - private readonly IAPIProvider api; private bool chatStarted; - public NotificationsClientConnector(IAPIProvider api) + protected NotificationsClientConnector(IAPIProvider api) : base(api) { - this.api = api; } public void StartChat() @@ -36,30 +34,18 @@ namespace osu.Game.Online.Notifications client.EnableChat = true; } - protected override async Task BuildConnectionAsync(CancellationToken cancellationToken) + protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken) { - var tcs = new TaskCompletionSource(); + var client = await BuildNotificationClientAsync(cancellationToken); - var req = new GetNotificationsRequest(); - req.Success += bundle => tcs.SetResult(bundle.Endpoint); - req.Failure += ex => tcs.SetException(ex); - api.Queue(req); + client.ChannelJoined = c => ChannelJoined?.Invoke(c); + client.NewMessages = m => NewMessages?.Invoke(m); + client.PresenceReceived = () => PresenceReceived?.Invoke(); + client.EnableChat = chatStarted; - string endpoint = await tcs.Task; - - ClientWebSocket socket = new ClientWebSocket(); - socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); - socket.Options.Proxy = WebRequest.DefaultWebProxy; - if (socket.Options.Proxy != null) - socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; - - return new NotificationsClient(socket, endpoint, api) - { - ChannelJoined = c => ChannelJoined?.Invoke(c), - NewMessages = m => NewMessages?.Invoke(m), - PresenceReceived = () => PresenceReceived?.Invoke(), - EnableChat = chatStarted - }; + return client; } + + protected abstract Task BuildNotificationClientAsync(CancellationToken cancellationToken); } } diff --git a/osu.Game/Online/Notifications/NotificationsClient_Processing.cs b/osu.Game/Online/Notifications/NotificationsClient_Processing.cs deleted file mode 100644 index 35cf737135..0000000000 --- a/osu.Game/Online/Notifications/NotificationsClient_Processing.cs +++ /dev/null @@ -1,105 +0,0 @@ -// 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 System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using osu.Game.Online.API.Requests; -using osu.Game.Online.Chat; - -namespace osu.Game.Online.Notifications -{ - public partial class NotificationsClient - { - public Action? ChannelJoined; - public Action>? NewMessages; - public Action? PresenceReceived; - - private bool enableChat; - private long lastMessageId; - - public bool EnableChat - { - get => enableChat; - set - { - if (enableChat == value) - return; - - enableChat = value; - Task.Run(startChatIfEnabledAsync); - } - } - - private async Task onConnectedAsync() - { - await startChatIfEnabledAsync(); - } - - private async Task startChatIfEnabledAsync() - { - if (!EnableChat) - return; - - await sendMessage(new StartChatRequest(), CancellationToken.None); - - var fetchReq = new GetUpdatesRequest(lastMessageId); - - fetchReq.Success += updates => - { - if (updates?.Presence != null) - { - foreach (var channel in updates.Presence) - handleJoinedChannel(channel); - - //todo: handle left channels - - handleMessages(updates.Messages); - } - - PresenceReceived?.Invoke(); - }; - - api.Queue(fetchReq); - } - - private Task onMessageReceivedAsync(SocketMessage message) - { - switch (message.Event) - { - case @"chat.message.new": - Debug.Assert(message.Data != null); - - NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString()); - Debug.Assert(messageData != null); - - List messages = messageData.Messages.Where(m => m.Sender.OnlineID != api.LocalUser.Value.OnlineID).ToList(); - - foreach (var msg in messages) - handleJoinedChannel(new Channel(msg.Sender) { Id = msg.ChannelId }); - - handleMessages(messages); - break; - } - - return Task.CompletedTask; - } - - private void handleJoinedChannel(Channel channel) - { - // we received this from the server so should mark the channel already joined. - channel.Joined.Value = true; - ChannelJoined?.Invoke(channel); - } - - private void handleMessages(List messages) - { - NewMessages?.Invoke(messages); - lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; - } - } -} diff --git a/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs b/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs new file mode 100644 index 0000000000..1c5559fcb4 --- /dev/null +++ b/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Online.API; + +namespace osu.Game.Online.Notifications.Polling +{ + /// + /// A notifications client which polls for new messages every second. + /// + public class PollingNotificationsClient : NotificationsClient + { + private readonly IAPIProvider api; + + public PollingNotificationsClient(IAPIProvider api) + : base(api) + { + this.api = api; + } + + public override Task ConnectAsync(CancellationToken cancellationToken) + { + Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested) + { + await api.PerformAsync(CreateFetchMessagesRequest()); + await Task.Delay(1000, cancellationToken); + } + }, cancellationToken); + + return Task.CompletedTask; + } + } +} diff --git a/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs b/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs new file mode 100644 index 0000000000..18a31ff061 --- /dev/null +++ b/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Online.API; + +namespace osu.Game.Online.Notifications.Polling +{ + /// + /// A connector for s that poll for new messages. + /// + public class PollingNotificationsClientConnector : NotificationsClientConnector + { + private readonly IAPIProvider api; + + public PollingNotificationsClientConnector(IAPIProvider api) + : base(api) + { + this.api = api; + } + + protected override Task BuildNotificationClientAsync(CancellationToken cancellationToken) + => Task.FromResult((NotificationsClient)new PollingNotificationsClient(api)); + } +} diff --git a/osu.Game/Online/Notifications/EndChatRequest.cs b/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs similarity index 65% rename from osu.Game/Online/Notifications/EndChatRequest.cs rename to osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs index f863511804..7f67587f5d 100644 --- a/osu.Game/Online/Notifications/EndChatRequest.cs +++ b/osu.Game/Online/Notifications/WebSocket/EndChatRequest.cs @@ -3,8 +3,11 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Notifications +namespace osu.Game.Online.Notifications.WebSocket { + /// + /// A websocket message notifying the server that the client no longer wants to receive chat messages. + /// [JsonObject(MemberSerialization.OptIn)] public class EndChatRequest : SocketMessage { diff --git a/osu.Game/Online/Notifications/NewChatMessageData.cs b/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs similarity index 83% rename from osu.Game/Online/Notifications/NewChatMessageData.cs rename to osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs index eda9a3a11b..850fbd226b 100644 --- a/osu.Game/Online/Notifications/NewChatMessageData.cs +++ b/osu.Game/Online/Notifications/WebSocket/NewChatMessageData.cs @@ -8,8 +8,11 @@ using Newtonsoft.Json; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -namespace osu.Game.Online.Notifications +namespace osu.Game.Online.Notifications.WebSocket { + /// + /// A websocket message sent from the server when new messages arrive. + /// [JsonObject(MemberSerialization.OptIn)] public class NewChatMessageData { diff --git a/osu.Game/Online/Notifications/SocketMessage.cs b/osu.Game/Online/Notifications/WebSocket/SocketMessage.cs similarity index 77% rename from osu.Game/Online/Notifications/SocketMessage.cs rename to osu.Game/Online/Notifications/WebSocket/SocketMessage.cs index c52072ccd5..666a9dd8a3 100644 --- a/osu.Game/Online/Notifications/SocketMessage.cs +++ b/osu.Game/Online/Notifications/WebSocket/SocketMessage.cs @@ -4,8 +4,11 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace osu.Game.Online.Notifications +namespace osu.Game.Online.Notifications.WebSocket { + /// + /// A websocket message, sent either from the client or server. + /// [JsonObject(MemberSerialization.OptIn)] public class SocketMessage { diff --git a/osu.Game/Online/Notifications/StartChatRequest.cs b/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs similarity index 66% rename from osu.Game/Online/Notifications/StartChatRequest.cs rename to osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs index d17644ee3b..9dd69a7377 100644 --- a/osu.Game/Online/Notifications/StartChatRequest.cs +++ b/osu.Game/Online/Notifications/WebSocket/StartChatRequest.cs @@ -3,8 +3,11 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Notifications +namespace osu.Game.Online.Notifications.WebSocket { + /// + /// A websocket message notifying the server that the client wants to receive chat messages. + /// [JsonObject(MemberSerialization.OptIn)] public class StartChatRequest : SocketMessage { diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs new file mode 100644 index 0000000000..cadeb8b9fa --- /dev/null +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -0,0 +1,148 @@ +// 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 System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Logging; +using osu.Game.Online.API; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.Notifications.WebSocket +{ + /// + /// A notifications client which receives events via a websocket. + /// + public class WebSocketNotificationsClient : NotificationsClient + { + private readonly ClientWebSocket socket; + private readonly string endpoint; + private readonly IAPIProvider api; + + public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api) + : base(api) + { + this.socket = socket; + this.endpoint = endpoint; + this.api = api; + } + + public override async Task ConnectAsync(CancellationToken cancellationToken) + { + await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false); + runReadLoop(cancellationToken); + await base.ConnectAsync(cancellationToken); + } + + protected override async Task StartChatAsync() + { + await sendMessage(new StartChatRequest(), CancellationToken.None); + await base.StartChatAsync(); + } + + private void runReadLoop(CancellationToken cancellationToken) => Task.Run((Func)(async () => + { + byte[] buffer = new byte[1024]; + StringBuilder messageResult = new StringBuilder(); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, cancellationToken); + + switch (result.MessageType) + { + case WebSocketMessageType.Text: + messageResult.Append(Encoding.UTF8.GetString(buffer[..result.Count])); + + if (result.EndOfMessage) + { + SocketMessage? message = JsonConvert.DeserializeObject(messageResult.ToString()); + messageResult.Clear(); + + Debug.Assert(message != null); + + if (message.Error != null) + { + Logger.Log($"{GetType().ReadableName()} error: {message.Error}", LoggingTarget.Network); + break; + } + + await onMessageReceivedAsync(message); + } + + break; + + case WebSocketMessageType.Binary: + throw new NotImplementedException(); + + case WebSocketMessageType.Close: + throw new Exception("Connection closed by remote host."); + } + } + catch (Exception ex) + { + await InvokeClosed(ex); + return; + } + } + }), cancellationToken); + + private async Task closeAsync() + { + try + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, @"Disconnecting", CancellationToken.None).ConfigureAwait(false); + } + catch + { + // Closure can fail if the connection is aborted. Don't really care since it's disposed anyway. + } + } + + private async Task sendMessage(SocketMessage message, CancellationToken cancellationToken) + { + if (socket.State != WebSocketState.Open) + return; + + await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken); + } + + private Task onMessageReceivedAsync(SocketMessage message) + { + switch (message.Event) + { + case @"chat.message.new": + Debug.Assert(message.Data != null); + + NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString()); + Debug.Assert(messageData != null); + + List messages = messageData.Messages.Where(m => m.Sender.OnlineID != api.LocalUser.Value.OnlineID).ToList(); + + foreach (var msg in messages) + HandleJoinedChannel(new Channel(msg.Sender) { Id = msg.ChannelId }); + + HandleMessages(messages); + break; + } + + return Task.CompletedTask; + } + + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + await closeAsync(); + socket.Dispose(); + } + } +} diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs new file mode 100644 index 0000000000..21335a3b59 --- /dev/null +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClientConnector.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Online.Notifications.WebSocket +{ + /// + /// A connector for s that receive events via a websocket. + /// + public class WebSocketNotificationsClientConnector : NotificationsClientConnector + { + private readonly IAPIProvider api; + + public WebSocketNotificationsClientConnector(IAPIProvider api) + : base(api) + { + this.api = api; + } + + protected override async Task BuildNotificationClientAsync(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + var req = new GetNotificationsRequest(); + req.Success += bundle => tcs.SetResult(bundle.Endpoint); + req.Failure += ex => tcs.SetException(ex); + api.Queue(req); + + string endpoint = await tcs.Task; + + ClientWebSocket socket = new ClientWebSocket(); + socket.Options.SetRequestHeader(@"Authorization", @$"Bearer {api.AccessToken}"); + socket.Options.Proxy = WebRequest.DefaultWebProxy; + if (socket.Options.Proxy != null) + socket.Options.Proxy.Credentials = CredentialCache.DefaultCredentials; + + return new WebSocketNotificationsClient(socket, endpoint, api); + } + } +} diff --git a/osu.Game/Online/SocketClient.cs b/osu.Game/Online/SocketClient.cs index 3b4aa1b49b..748e77fc0b 100644 --- a/osu.Game/Online/SocketClient.cs +++ b/osu.Game/Online/SocketClient.cs @@ -13,7 +13,7 @@ namespace osu.Game.Online protected Task InvokeClosed(Exception? exception) => Closed?.Invoke(exception) ?? Task.CompletedTask; - public abstract Task StartAsync(CancellationToken cancellationToken); + public abstract Task ConnectAsync(CancellationToken cancellationToken); public virtual ValueTask DisposeAsync() { diff --git a/osu.Game/Online/SocketClientConnector.cs b/osu.Game/Online/SocketClientConnector.cs index 823e724ef9..4ce5c75ba8 100644 --- a/osu.Game/Online/SocketClientConnector.cs +++ b/osu.Game/Online/SocketClientConnector.cs @@ -92,7 +92,7 @@ namespace osu.Game.Online cancellationToken.ThrowIfCancellationRequested(); - await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false); + await CurrentConnection.ConnectAsync(cancellationToken).ConfigureAwait(false); Logger.Log($"{ClientName} connected!", LoggingTarget.Network); isConnected.Value = true; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7a42007fb8..df3000a547 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -45,6 +45,7 @@ using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications; +using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; @@ -882,7 +883,7 @@ namespace osu.Game loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); - loadComponentSingleFile(notificationsClient = new NotificationsClientConnector(API), AddInternal, true); + loadComponentSingleFile(notificationsClient = new PollingNotificationsClientConnector(API), AddInternal, true); loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); From 169bcc265416b52324ab12d177f32b771fa09a0d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 18:08:08 +0900 Subject: [PATCH 006/152] Use polling connector in tests --- .../Chat/TestSceneChannelManager.cs | 3 ++- .../Visual/Online/TestSceneChatLink.cs | 5 ++++- .../Visual/Online/TestSceneChatOverlay.cs | 3 ++- .../Visual/Online/TestSceneMessageNotifier.cs | 3 ++- .../Online/TestSceneStandAloneChatDisplay.cs | 5 ++++- .../Components/TournamentMatchChatDisplay.cs | 3 ++- osu.Game/Online/Chat/ChannelManager.cs | 14 ++++++++++---- osu.Game/Online/SocketClientConnector.cs | 19 +++++++++++++++---- osu.Game/OsuGame.cs | 6 +++--- 9 files changed, 44 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index e7eb06c795..84609a2733 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -13,6 +13,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Notifications.Polling; using osu.Game.Tests.Visual; namespace osu.Game.Tests.Chat @@ -151,7 +152,7 @@ namespace osu.Game.Tests.Chat public ChannelManagerContainer(IAPIProvider apiProvider) { - InternalChild = ChannelManager = new ChannelManager(apiProvider); + InternalChild = ChannelManager = new ChannelManager(apiProvider, new PollingNotificationsClientConnector(apiProvider)); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index a537f0660c..63526d4278 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays.Chat; using osuTK.Graphics; @@ -41,11 +42,13 @@ namespace osu.Game.Tests.Visual.Online { linkColour = colours.Blue; - var chatManager = new ChannelManager(API); + var chatManager = new ChannelManager(API, new PollingNotificationsClientConnector(API)); BindableList availableChannels = (BindableList)chatManager.AvailableChannels; availableChannels.Add(new Channel { Name = "#english" }); availableChannels.Add(new Channel { Name = "#japanese" }); Dependencies.Cache(chatManager); + + Add(chatManager); } [SetUp] diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 0b982a5745..dee258f747 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -24,6 +24,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays; using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Listing; @@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { - (typeof(ChannelManager), channelManager = new ChannelManager(API)), + (typeof(ChannelManager), channelManager = new ChannelManager(API, new PollingNotificationsClientConnector(API))), }, Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 57514cdf37..6d83fb3123 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osuTK.Input; @@ -250,7 +251,7 @@ namespace osu.Game.Tests.Visual.Online public TestContainer(IAPIProvider api, Channel[] channels) { this.channels = channels; - ChannelManager = new ChannelManager(api); + ChannelManager = new ChannelManager(api, new PollingNotificationsClientConnector(api)); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 292facab11..f1daa05e08 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -15,6 +15,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays.Chat; using osuTK.Input; @@ -56,7 +57,9 @@ namespace osu.Game.Tests.Visual.Online protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - Add(channelManager = new ChannelManager(parent.Get())); + var api = parent.Get(); + + Add(channelManager = new ChannelManager(api, new PollingNotificationsClientConnector(api))); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index ca2b400e8b..99cd9beecf 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Online.Notifications.WebSocket; using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -48,7 +49,7 @@ namespace osu.Game.Tournament.Components if (manager == null) { - AddInternal(manager = new ChannelManager(api)); + AddInternal(manager = new ChannelManager(api, new WebSocketNotificationsClientConnector(api))); Channel.BindTo(manager.CurrentChannel); } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index b63f841f59..ab3ac6f692 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -65,20 +65,20 @@ namespace osu.Game.Online.Chat public IBindableList AvailableChannels => availableChannels; private readonly IAPIProvider api; + private readonly NotificationsClientConnector connector; [Resolved] private UserLookupCache users { get; set; } - [Resolved] - private NotificationsClientConnector connector { get; set; } - private readonly IBindable apiState = new Bindable(); private bool channelsInitialised; private ScheduledDelegate ackDelegate; - public ChannelManager(IAPIProvider api) + public ChannelManager(IAPIProvider api, NotificationsClientConnector connector) { this.api = api; + this.connector = connector; + CurrentChannel.ValueChanged += currentChannelChanged; } @@ -603,6 +603,12 @@ namespace osu.Game.Online.Chat api.Queue(req); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + connector?.Dispose(); + } } /// diff --git a/osu.Game/Online/SocketClientConnector.cs b/osu.Game/Online/SocketClientConnector.cs index 4ce5c75ba8..c6d5601c1f 100644 --- a/osu.Game/Online/SocketClientConnector.cs +++ b/osu.Game/Online/SocketClientConnector.cs @@ -6,13 +6,12 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Online.API; namespace osu.Game.Online { - public abstract class SocketClientConnector : Component + public abstract class SocketClientConnector : IDisposable { /// /// Whether this is connected to the hub, use to access the connection, if this is true. @@ -173,11 +172,23 @@ namespace osu.Game.Online public override string ToString() => $"{ClientName} ({(IsConnected.Value ? "connected" : "not connected")})"; - protected override void Dispose(bool isDisposing) + private bool isDisposed; + + protected virtual void Dispose(bool isDisposing) { - base.Dispose(isDisposing); + if (isDisposed) + return; + apiState.UnbindAll(); cancelExistingConnect(); + + isDisposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index df3000a547..4ceefbf1fd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -45,7 +45,7 @@ using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications; -using osu.Game.Online.Notifications.Polling; +using osu.Game.Online.Notifications.WebSocket; using osu.Game.Overlays; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; @@ -757,6 +757,7 @@ namespace osu.Game BackButton.Receptor receptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); + dependencies.CacheAs(notificationsClient = new WebSocketNotificationsClientConnector(API)); var sessionIdleTracker = new GameIdleTracker(300000); sessionIdleTracker.IsIdle.BindValueChanged(idle => @@ -883,8 +884,7 @@ namespace osu.Game loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); - loadComponentSingleFile(notificationsClient = new PollingNotificationsClientConnector(API), AddInternal, true); - loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true); + loadComponentSingleFile(channelManager = new ChannelManager(API, notificationsClient), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true); From ab78cde2d471fe3478f0a5ac4efc074b102c4f2c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 18:37:43 +0900 Subject: [PATCH 007/152] Fix crossthread mutations --- osu.Game/Online/Chat/ChannelManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index ab3ac6f692..a901c15bf4 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -85,9 +85,9 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load() { - connector.ChannelJoined += ch => joinChannel(ch); - connector.NewMessages += addMessages; - connector.PresenceReceived += () => + connector.ChannelJoined += ch => Schedule(() => joinChannel(ch)); + connector.NewMessages += msgs => Schedule(() => addMessages(msgs)); + connector.PresenceReceived += () => Schedule(() => { if (!channelsInitialised) { @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat // we want this to run after the first presence so we can see if the user is in any channels already. initializeChannels(); } - }; + }); connector.StartChat(); From d3173ab1bd836d4061325beb67b8a40eb98cd658 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Oct 2022 18:54:34 +0900 Subject: [PATCH 008/152] Remove weird cast --- .../Notifications/WebSocket/WebSocketNotificationsClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index cadeb8b9fa..ff0941ecba 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online.Notifications.WebSocket await base.StartChatAsync(); } - private void runReadLoop(CancellationToken cancellationToken) => Task.Run((Func)(async () => + private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () => { byte[] buffer = new byte[1024]; StringBuilder messageResult = new StringBuilder(); @@ -94,7 +94,7 @@ namespace osu.Game.Online.Notifications.WebSocket return; } } - }), cancellationToken); + }, cancellationToken); private async Task closeAsync() { From 5b25ef5f2fcc7d4c1db03cd529733fb8375237c4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Nov 2022 21:34:34 +0900 Subject: [PATCH 009/152] Construct notifications client via IAPIProvider --- osu.Game.Tests/Chat/TestSceneChannelManager.cs | 3 +-- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 3 +-- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 3 +-- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 3 +-- .../Visual/Online/TestSceneStandAloneChatDisplay.cs | 3 +-- osu.Game/Online/API/APIAccess.cs | 5 +++++ osu.Game/Online/API/DummyAPIAccess.cs | 4 ++++ osu.Game/Online/API/IAPIProvider.cs | 6 ++++++ osu.Game/OsuGame.cs | 3 +-- 9 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 84609a2733..86be638781 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -13,7 +13,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Online.Notifications.Polling; using osu.Game.Tests.Visual; namespace osu.Game.Tests.Chat @@ -152,7 +151,7 @@ namespace osu.Game.Tests.Chat public ChannelManagerContainer(IAPIProvider apiProvider) { - InternalChild = ChannelManager = new ChannelManager(apiProvider, new PollingNotificationsClientConnector(apiProvider)); + InternalChild = ChannelManager = new ChannelManager(apiProvider, apiProvider.GetNotificationsConnector()); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 63526d4278..5c46cd96be 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays.Chat; using osuTK.Graphics; @@ -42,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online { linkColour = colours.Blue; - var chatManager = new ChannelManager(API, new PollingNotificationsClientConnector(API)); + var chatManager = new ChannelManager(API, API.GetNotificationsConnector()); BindableList availableChannels = (BindableList)chatManager.AvailableChannels; availableChannels.Add(new Channel { Name = "#english" }); availableChannels.Add(new Channel { Name = "#japanese" }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index dee258f747..6b2124f1f8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -24,7 +24,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays; using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Listing; @@ -60,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { - (typeof(ChannelManager), channelManager = new ChannelManager(API, new PollingNotificationsClientConnector(API))), + (typeof(ChannelManager), channelManager = new ChannelManager(API, API.GetNotificationsConnector())), }, Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index 6d83fb3123..cb93812bc2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -16,7 +16,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osuTK.Input; @@ -251,7 +250,7 @@ namespace osu.Game.Tests.Visual.Online public TestContainer(IAPIProvider api, Channel[] channels) { this.channels = channels; - ChannelManager = new ChannelManager(api, new PollingNotificationsClientConnector(api)); + ChannelManager = new ChannelManager(api, api.GetNotificationsConnector()); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index f1daa05e08..07196cc92a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -15,7 +15,6 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Notifications.Polling; using osu.Game.Overlays.Chat; using osuTK.Input; @@ -59,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online { var api = parent.Get(); - Add(channelManager = new ChannelManager(api, new PollingNotificationsClientConnector(api))); + Add(channelManager = new ChannelManager(api, api.GetNotificationsConnector())); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a0c8e0d555..8ac2e2d453 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -20,6 +20,8 @@ using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Notifications; +using osu.Game.Online.Notifications.WebSocket; using osu.Game.Users; namespace osu.Game.Online.API @@ -299,6 +301,9 @@ namespace osu.Game.Online.API public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack); + public NotificationsClientConnector GetNotificationsConnector() => + new WebSocketNotificationsClientConnector(this); + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Debug.Assert(State.Value == APIState.Offline); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7dc34d1293..865e1e0a70 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Notifications; +using osu.Game.Online.Notifications.Polling; using osu.Game.Users; namespace osu.Game.Online.API @@ -115,6 +117,8 @@ namespace osu.Game.Online.API public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; + public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this); + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Thread.Sleep(200); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index a90b11e354..6054effaa1 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -5,6 +5,7 @@ using System; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Notifications; using osu.Game.Users; namespace osu.Game.Online.API @@ -112,6 +113,11 @@ namespace osu.Game.Online.API /// Whether to use MessagePack for serialisation if available on this platform. IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true); + /// + /// Constructs a new . + /// + NotificationsClientConnector GetNotificationsConnector(); + /// /// Create a new user account. This is a blocking operation. /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c1db9630a0..7384c2a297 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -45,7 +45,6 @@ using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications; -using osu.Game.Online.Notifications.WebSocket; using osu.Game.Overlays; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; @@ -759,7 +758,7 @@ namespace osu.Game BackButton.Receptor receptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); - dependencies.CacheAs(notificationsClient = new WebSocketNotificationsClientConnector(API)); + dependencies.CacheAs(notificationsClient = API.GetNotificationsConnector()); var sessionIdleTracker = new GameIdleTracker(300000); sessionIdleTracker.IsIdle.BindValueChanged(idle => From 8ac2075c61727961b1fe43554bc077abdc7ee7b5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 10:04:24 +0900 Subject: [PATCH 010/152] Fix possible threading issues Not really sure of the best way to handle this in general. It could be argued that this should be a `Component` type and the bindable bound in `LoadComplete()`... --- osu.Game/Online/Notifications/NotificationsClient.cs | 6 +++--- .../Notifications/Polling/PollingNotificationsClient.cs | 5 +---- .../Polling/PollingNotificationsClientConnector.cs | 5 +---- osu.Game/Online/SocketClientConnector.cs | 7 +++++-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index 6d9226ca17..8c41ae10d7 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -21,14 +21,14 @@ namespace osu.Game.Online.Notifications public Action>? NewMessages; public Action? PresenceReceived; - private readonly IAPIProvider api; + protected readonly IAPIProvider API; private bool enableChat; private long lastMessageId; protected NotificationsClient(IAPIProvider api) { - this.api = api; + API = api; } public bool EnableChat @@ -54,7 +54,7 @@ namespace osu.Game.Online.Notifications protected virtual Task StartChatAsync() { - api.Queue(CreateFetchMessagesRequest(0)); + API.Queue(CreateFetchMessagesRequest(0)); return Task.CompletedTask; } diff --git a/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs b/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs index 1c5559fcb4..2f343abf86 100644 --- a/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs +++ b/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs @@ -12,12 +12,9 @@ namespace osu.Game.Online.Notifications.Polling /// public class PollingNotificationsClient : NotificationsClient { - private readonly IAPIProvider api; - public PollingNotificationsClient(IAPIProvider api) : base(api) { - this.api = api; } public override Task ConnectAsync(CancellationToken cancellationToken) @@ -26,7 +23,7 @@ namespace osu.Game.Online.Notifications.Polling { while (!cancellationToken.IsCancellationRequested) { - await api.PerformAsync(CreateFetchMessagesRequest()); + await API.PerformAsync(CreateFetchMessagesRequest()); await Task.Delay(1000, cancellationToken); } }, cancellationToken); diff --git a/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs b/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs index 18a31ff061..ff3f30edcd 100644 --- a/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs @@ -12,15 +12,12 @@ namespace osu.Game.Online.Notifications.Polling /// public class PollingNotificationsClientConnector : NotificationsClientConnector { - private readonly IAPIProvider api; - public PollingNotificationsClientConnector(IAPIProvider api) : base(api) { - this.api = api; } protected override Task BuildNotificationClientAsync(CancellationToken cancellationToken) - => Task.FromResult((NotificationsClient)new PollingNotificationsClient(api)); + => Task.FromResult((NotificationsClient)new PollingNotificationsClient(API)); } } diff --git a/osu.Game/Online/SocketClientConnector.cs b/osu.Game/Online/SocketClientConnector.cs index c6d5601c1f..abf19fbba3 100644 --- a/osu.Game/Online/SocketClientConnector.cs +++ b/osu.Game/Online/SocketClientConnector.cs @@ -23,18 +23,21 @@ namespace osu.Game.Online /// public SocketClient? CurrentConnection { get; private set; } + protected readonly IAPIProvider API; + + private readonly IBindable apiState = new Bindable(); private readonly Bindable isConnected = new Bindable(); private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); - private readonly IBindable apiState = new Bindable(); - /// /// Constructs a new . /// /// An API provider used to react to connection state changes. protected SocketClientConnector(IAPIProvider api) { + API = api; + apiState.BindTo(api.State); apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true); } From fe66b207020fb816b1395fc50d507717ef8bcf16 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 15:22:46 +0900 Subject: [PATCH 011/152] Fix one more case of constructing connector directly --- osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 99cd9beecf..04dfe5f7c2 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tournament.Components if (manager == null) { - AddInternal(manager = new ChannelManager(api, new WebSocketNotificationsClientConnector(api))); + AddInternal(manager = new ChannelManager(api, api.GetNotificationsConnector())); Channel.BindTo(manager.CurrentChannel); } From 58c6b026ae503097b86f8c8f139912042fa3360a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 15:23:11 +0900 Subject: [PATCH 012/152] Remove unused using --- osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 04dfe5f7c2..48ff45974d 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.Chat; -using osu.Game.Online.Notifications.WebSocket; using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; From 695104a66666a46f5a1b9429694aa6071320dc62 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 16:59:03 +0900 Subject: [PATCH 013/152] Fix TestSceneChatOverlay messages not being unique --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 6b2124f1f8..fd1bd4f0df 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.Online private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 }; private Channel[] testChannels; + private Message[] initialMessages; private Channel testChannel1 => testChannels[0]; private Channel testChannel2 => testChannels[1]; @@ -49,10 +50,14 @@ namespace osu.Game.Tests.Visual.Online [Resolved] private OsuConfigManager config { get; set; } = null!; + private int currentMessageId; + [SetUp] public void SetUp() => Schedule(() => { + currentMessageId = 0; testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); + initialMessages = testChannels.SelectMany(createChannelMessages).ToArray(); Child = new DependencyProvidingContainer { @@ -99,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online return true; case GetMessagesRequest getMessages: - getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel)); + getMessages.TriggerSuccess(initialMessages.ToList()); return true; case GetUserRequest getUser: @@ -546,7 +551,7 @@ namespace osu.Game.Tests.Visual.Online private List createChannelMessages(Channel channel) { - var message = new Message + var message = new Message(currentMessageId++) { ChannelId = channel.Id, Content = $"Hello, this is a message in {channel.Name}", From f688ed12d0b611a5955b817e127a3acd32fc6f98 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 17:00:47 +0900 Subject: [PATCH 014/152] Add test for removing chat messages --- .../Visual/Online/TestSceneChatOverlay.cs | 30 +++++++++++++++++++ osu.Game/Online/Chat/Channel.cs | 14 +++++++++ 2 files changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index fd1bd4f0df..260b47e836 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.Online private ChannelManager channelManager; private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 }; + private readonly APIUser testUser1 = new APIUser { Username = "test user", Id = 5071480 }; private Channel[] testChannels; private Message[] initialMessages; @@ -500,6 +501,35 @@ namespace osu.Game.Tests.Visual.Online waitForChannel1Visible(); } + [Test] + public void TestRemoveMessages() + { + AddStep("Show overlay with channel", () => + { + chatOverlay.Show(); + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1); + }); + + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + waitForChannel1Visible(); + + AddStep("Send message from another user", () => + { + testChannel1.AddNewMessages(new Message + { + ChannelId = testChannel1.Id, + Content = "Message from another user", + Timestamp = DateTimeOffset.Now, + Sender = testUser1, + }); + }); + + AddStep("Remove messages from other user", () => + { + testChannel1.RemoveMessagesFromUser(testUser.Id); + }); + } + private void joinTestChannel(int i) { AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i])); diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index f51ea3e8d6..17a6a430b6 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -149,6 +149,20 @@ namespace osu.Game.Online.Chat NewMessagesArrived?.Invoke(messages); } + public void RemoveMessagesFromUser(int userId) + { + for (int i = 0; i < Messages.Count; i++) + { + var message = Messages[i]; + + if (message.SenderId == userId) + { + Messages.RemoveAt(i--); + MessageRemoved?.Invoke(message); + } + } + } + /// /// Replace or remove a message from the channel. /// From 063a8bdf9e3f12461a6201b33ae182be35a4a102 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 17:13:14 +0900 Subject: [PATCH 015/152] Remove messages from silenced users --- .../Chat/TestSceneChannelManager.cs | 29 ++++++++++++++++ .../Online/API/Requests/ChatAckRequest.cs | 6 ++++ .../API/Requests/Responses/ChatSilence.cs | 2 +- osu.Game/Online/Chat/ChannelManager.cs | 34 +++++++++++++++++-- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 86be638781..f559b0db73 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -23,6 +23,7 @@ namespace osu.Game.Tests.Chat private ChannelManager channelManager; private int currentMessageId; private List sentMessages; + private List silencedUserIds; [SetUp] public void Setup() => Schedule(() => @@ -39,6 +40,7 @@ namespace osu.Game.Tests.Chat { currentMessageId = 0; sentMessages = new List(); + silencedUserIds = new List(); ((DummyAPIAccess)API).HandleRequest = req => { @@ -55,6 +57,11 @@ namespace osu.Game.Tests.Chat case MarkChannelAsReadRequest markRead: handleMarkChannelAsReadRequest(markRead); return true; + + case ChatAckRequest ack: + ack.TriggerSuccess(new ChatAckResponse { Silences = silencedUserIds.Select(u => new ChatSilence { UserId = u }).ToList() }); + silencedUserIds.Clear(); + return true; } return false; @@ -106,6 +113,28 @@ namespace osu.Game.Tests.Chat AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id); } + [Test] + public void TestSilencedUsersAreRemoved() + { + Channel channel = null; + + AddStep("join channel and select it", () => + { + channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public)); + channelManager.CurrentChannel.Value = channel; + }); + + AddStep("post message", () => channelManager.PostMessage("Definitely something bad")); + + AddStep("mark user as silenced and send ack request", () => + { + silencedUserIds.Add(API.LocalUser.Value.OnlineID); + channelManager.SendAck(); + }); + + AddAssert("channel has no more messages", () => channel.Messages, () => Is.Empty); + } + private void handlePostMessageRequest(PostMessageRequest request) { var message = new Message(++currentMessageId) diff --git a/osu.Game/Online/API/Requests/ChatAckRequest.cs b/osu.Game/Online/API/Requests/ChatAckRequest.cs index f09df4908e..78f51e21c0 100644 --- a/osu.Game/Online/API/Requests/ChatAckRequest.cs +++ b/osu.Game/Online/API/Requests/ChatAckRequest.cs @@ -9,10 +9,16 @@ namespace osu.Game.Online.API.Requests { public class ChatAckRequest : APIRequest { + public long SinceMessageId; + public uint? SinceSilenceId; + protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; + req.AddParameter(@"since", SinceMessageId.ToString()); + if (SinceSilenceId != null) + req.AddParameter(@"history_since", SinceSilenceId.Value.ToString()); return req; } diff --git a/osu.Game/Online/API/Requests/Responses/ChatSilence.cs b/osu.Game/Online/API/Requests/Responses/ChatSilence.cs index 45fd6e1ba3..afb44e385e 100644 --- a/osu.Game/Online/API/Requests/Responses/ChatSilence.cs +++ b/osu.Game/Online/API/Requests/Responses/ChatSilence.cs @@ -12,6 +12,6 @@ namespace osu.Game.Online.API.Requests.Responses public uint Id { get; set; } [JsonProperty("user_id")] - public uint UserId { get; set; } + public int UserId { get; set; } } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index a901c15bf4..b9a0aacf5a 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -74,6 +74,9 @@ namespace osu.Game.Online.Chat private bool channelsInitialised; private ScheduledDelegate ackDelegate; + private long lastMessageId; + private uint? lastSilenceId; + public ChannelManager(IAPIProvider api, NotificationsClientConnector connector) { this.api = api; @@ -106,8 +109,7 @@ namespace osu.Game.Online.Chat if (status.NewValue == APIState.Online) { - Scheduler.Add(ackDelegate = new ScheduledDelegate(() => api.Queue(new ChatAckRequest()), 0, 60000)); - // Todo: Handle silences. + Scheduler.Add(ackDelegate = new ScheduledDelegate(SendAck, 0, 60000)); } }, true); } @@ -342,6 +344,8 @@ namespace osu.Game.Online.Chat foreach (var group in messages.GroupBy(m => m.ChannelId)) channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); + + lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; } private void initializeChannels() @@ -391,6 +395,32 @@ namespace osu.Game.Online.Chat api.Queue(fetchInitialMsgReq); } + /// + /// Sends an acknowledgement request to the API. + /// This marks the user as online to receive messages from public channels, while also returning a list of silenced users. + /// It needs to be called at least once every 10 minutes. + /// + public void SendAck() + { + var req = new ChatAckRequest + { + SinceMessageId = lastMessageId, + SinceSilenceId = lastSilenceId + }; + + req.Success += ack => + { + foreach (var silence in ack.Silences) + { + foreach (var channel in JoinedChannels) + channel.RemoveMessagesFromUser(silence.UserId); + lastSilenceId = Math.Max(lastSilenceId ?? 0, silence.Id); + } + }; + + api.Queue(req); + } + /// /// Find an existing channel instance for the provided channel. Lookup is performed basd on ID. /// The provided channel may be used if an existing instance is not found. From 4f8e912f063b08ed8be5afb2a513d8c804092685 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Nov 2022 17:53:19 +0900 Subject: [PATCH 016/152] Fix `APINotification` parsing failing --- osu.Game/Online/API/Requests/Responses/APINotification.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APINotification.cs b/osu.Game/Online/API/Requests/Responses/APINotification.cs index 2d9122c04b..de856c0333 100644 --- a/osu.Game/Online/API/Requests/Responses/APINotification.cs +++ b/osu.Game/Online/API/Requests/Responses/APINotification.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace osu.Game.Online.API.Requests.Responses { @@ -32,6 +32,6 @@ namespace osu.Game.Online.API.Requests.Responses public bool IsRead { get; set; } [JsonProperty(@"details")] - public Dictionary? Details { get; set; } + public JObject? Details { get; set; } } } From 58396d49dc9e44a7a3dcec75a969851fd7856451 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 16:42:59 +0900 Subject: [PATCH 017/152] Fix handling of local echo deduplication --- .../CreateNewPrivateMessageRequest.cs | 1 + .../Online/API/Requests/PostMessageRequest.cs | 1 + osu.Game/Online/Chat/Channel.cs | 8 ++++ osu.Game/Online/Chat/ChannelManager.cs | 48 ++++++++++++------- osu.Game/Online/Chat/Message.cs | 6 +++ .../WebSocket/WebSocketNotificationsClient.cs | 10 ++-- 6 files changed, 51 insertions(+), 23 deletions(-) diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs index dea94bfce2..6b7192dbf4 100644 --- a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs +++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs @@ -28,6 +28,7 @@ namespace osu.Game.Online.API.Requests req.AddParameter(@"target_id", user.Id.ToString()); req.AddParameter(@"message", message.Content); req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant()); + req.AddParameter(@"uuid", message.Uuid); return req; } diff --git a/osu.Game/Online/API/Requests/PostMessageRequest.cs b/osu.Game/Online/API/Requests/PostMessageRequest.cs index 7b20bd9ad5..e3709d8f13 100644 --- a/osu.Game/Online/API/Requests/PostMessageRequest.cs +++ b/osu.Game/Online/API/Requests/PostMessageRequest.cs @@ -25,6 +25,7 @@ namespace osu.Game.Online.API.Requests req.Method = HttpMethod.Post; req.AddParameter(@"is_action", Message.IsAction.ToString().ToLowerInvariant()); req.AddParameter(@"message", Message.Content); + req.AddParameter(@"uuid", Message.Uuid); return req; } diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index f51ea3e8d6..9bfaecc46b 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -134,6 +134,14 @@ namespace osu.Game.Online.Chat /// public void AddNewMessages(params Message[] messages) { + foreach (var m in messages) + { + LocalEchoMessage localEcho = pendingMessages.FirstOrDefault(local => local.Uuid == m.Uuid); + + if (localEcho != null) + ReplaceMessage(localEcho, m); + } + messages = messages.Except(Messages).ToArray(); if (messages.Length == 0) return; diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index a901c15bf4..05e2fb79e8 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -85,8 +85,21 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load() { - connector.ChannelJoined += ch => Schedule(() => joinChannel(ch)); + connector.ChannelJoined += ch => Schedule(() => + { + var localChannel = getChannel(ch); + + if (localChannel != ch) + { + localChannel.Joined.Value = true; + localChannel.Id = ch.Id; + } + + joinChannel(localChannel); + }); + connector.NewMessages += msgs => Schedule(() => addMessages(msgs)); + connector.PresenceReceived += () => Schedule(() => { if (!channelsInitialised) @@ -189,7 +202,8 @@ namespace osu.Game.Online.Chat Timestamp = DateTimeOffset.Now, ChannelId = target.Id, IsAction = isAction, - Content = text + Content = text, + Uuid = Guid.NewGuid().ToString() }; target.AddLocalEcho(message); @@ -199,13 +213,7 @@ namespace osu.Game.Online.Chat { var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message); - createNewPrivateMessageRequest.Success += createRes => - { - target.Id = createRes.ChannelID; - target.ReplaceMessage(message, createRes.Message); - dequeueAndRun(); - }; - + createNewPrivateMessageRequest.Success += _ => dequeueAndRun(); createNewPrivateMessageRequest.Failure += exception => { handlePostException(exception); @@ -219,12 +227,7 @@ namespace osu.Game.Online.Chat var req = new PostMessageRequest(message); - req.Success += m => - { - target.ReplaceMessage(message, m); - dequeueAndRun(); - }; - + req.Success += m => dequeueAndRun(); req.Failure += exception => { handlePostException(exception); @@ -403,7 +406,20 @@ namespace osu.Game.Online.Chat { Channel found = null; - bool lookupCondition(Channel ch) => lookup.Id > 0 ? ch.Id == lookup.Id : lookup.Name == ch.Name; + bool lookupCondition(Channel ch) + { + // If both channels have an id, use that. + if (lookup.Id > 0 && ch.Id > 0) + return ch.Id == lookup.Id; + + // In the case that the local echo is received in a new channel (i.e. one that does not yet have an ID), + // then we need to check for any existing channel with the message containing the same message matched by UUID. + if (lookup.Messages.Count > 0 && ch.Messages.Any(m => m.Uuid == lookup.Messages.Last().Uuid)) + return true; + + // As a last resort, fallback to matching by name. + return lookup.Name == ch.Name; + } var available = AvailableChannels.FirstOrDefault(lookupCondition); if (available != null) diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 25c5b0853f..9f6f9c8d6b 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -37,6 +37,12 @@ namespace osu.Game.Online.Chat set => Sender = new APIUser { Id = value }; } + /// + /// A unique identifier for this message. Sent to and from osu!web to use for deduplication. + /// + [JsonProperty(@"uuid")] + public string Uuid { get; set; } = string.Empty; + [JsonConstructor] public Message() { diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index ff0941ecba..0a93081915 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -2,9 +2,7 @@ // 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 System.Net.WebSockets; using System.Text; using System.Threading; @@ -126,12 +124,10 @@ namespace osu.Game.Online.Notifications.WebSocket NewChatMessageData? messageData = JsonConvert.DeserializeObject(message.Data.ToString()); Debug.Assert(messageData != null); - List messages = messageData.Messages.Where(m => m.Sender.OnlineID != api.LocalUser.Value.OnlineID).ToList(); + foreach (var msg in messageData.Messages) + HandleJoinedChannel(new Channel(msg.Sender) { Id = msg.ChannelId, Messages = { msg } }); - foreach (var msg in messages) - HandleJoinedChannel(new Channel(msg.Sender) { Id = msg.ChannelId }); - - HandleMessages(messages); + HandleMessages(messageData.Messages); break; } From 72745656e71ef3def43ca65b709a6c2f4000b1ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 18:48:34 +0900 Subject: [PATCH 018/152] Remove StartChat()/chat enablement --- osu.Game/Online/Chat/ChannelManager.cs | 2 -- .../Notifications/NotificationsClient.cs | 24 +------------------ .../NotificationsClientConnector.cs | 11 --------- .../WebSocket/WebSocketNotificationsClient.cs | 13 ++++------ 4 files changed, 5 insertions(+), 45 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 05e2fb79e8..77d53911d9 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -110,8 +110,6 @@ namespace osu.Game.Online.Chat } }); - connector.StartChat(); - apiState.BindTo(api.State); apiState.BindValueChanged(status => { diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index 5c575e828d..cb5e94fb1d 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -23,7 +23,6 @@ namespace osu.Game.Online.Notifications protected readonly IAPIProvider API; - private bool enableChat; private long lastMessageId; protected NotificationsClient(IAPIProvider api) @@ -31,28 +30,7 @@ namespace osu.Game.Online.Notifications API = api; } - public bool EnableChat - { - get => enableChat; - set - { - if (enableChat == value) - return; - - enableChat = value; - - if (EnableChat) - Task.Run(StartChatAsync); - } - } - - public override async Task ConnectAsync(CancellationToken cancellationToken) - { - if (EnableChat) - await StartChatAsync(); - } - - protected virtual Task StartChatAsync() + public override Task ConnectAsync(CancellationToken cancellationToken) { API.Queue(CreateFetchMessagesRequest(0)); return Task.CompletedTask; diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs index 5aa49a7783..d4c846a7a2 100644 --- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs @@ -19,21 +19,11 @@ namespace osu.Game.Online.Notifications public event Action>? NewMessages; public event Action? PresenceReceived; - private bool chatStarted; - protected NotificationsClientConnector(IAPIProvider api) : base(api) { } - public void StartChat() - { - chatStarted = true; - - if (CurrentConnection is NotificationsClient client) - client.EnableChat = true; - } - protected sealed override async Task BuildConnectionAsync(CancellationToken cancellationToken) { var client = await BuildNotificationClientAsync(cancellationToken); @@ -41,7 +31,6 @@ namespace osu.Game.Online.Notifications client.ChannelJoined = c => ChannelJoined?.Invoke(c); client.NewMessages = m => NewMessages?.Invoke(m); client.PresenceReceived = () => PresenceReceived?.Invoke(); - client.EnableChat = chatStarted; return client; } diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index 0a93081915..bc4ceea993 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -22,27 +22,22 @@ namespace osu.Game.Online.Notifications.WebSocket { private readonly ClientWebSocket socket; private readonly string endpoint; - private readonly IAPIProvider api; public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api) : base(api) { this.socket = socket; this.endpoint = endpoint; - this.api = api; } public override async Task ConnectAsync(CancellationToken cancellationToken) { await socket.ConnectAsync(new Uri(endpoint), cancellationToken).ConfigureAwait(false); - runReadLoop(cancellationToken); - await base.ConnectAsync(cancellationToken); - } - - protected override async Task StartChatAsync() - { await sendMessage(new StartChatRequest(), CancellationToken.None); - await base.StartChatAsync(); + + runReadLoop(cancellationToken); + + await base.ConnectAsync(cancellationToken); } private void runReadLoop(CancellationToken cancellationToken) => Task.Run(async () => From fa18b5f7014671f13c0f4930b3ae740755952f07 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 18:51:00 +0900 Subject: [PATCH 019/152] Construct notifications client inside ChannelManager --- osu.Game.Tests/Chat/TestSceneChannelManager.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs | 2 +- .../Visual/Online/TestSceneStandAloneChatDisplay.cs | 2 +- .../Components/TournamentMatchChatDisplay.cs | 2 +- osu.Game/Online/Chat/ChannelManager.cs | 5 +++-- osu.Game/OsuGame.cs | 6 +----- 8 files changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index 86be638781..e7eb06c795 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Chat public ChannelManagerContainer(IAPIProvider apiProvider) { - InternalChild = ChannelManager = new ChannelManager(apiProvider, apiProvider.GetNotificationsConnector()); + InternalChild = ChannelManager = new ChannelManager(apiProvider); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 5c46cd96be..de44986001 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online { linkColour = colours.Blue; - var chatManager = new ChannelManager(API, API.GetNotificationsConnector()); + var chatManager = new ChannelManager(API); BindableList availableChannels = (BindableList)chatManager.AvailableChannels; availableChannels.Add(new Channel { Name = "#english" }); availableChannels.Add(new Channel { Name = "#japanese" }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 6b2124f1f8..0b982a5745 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { - (typeof(ChannelManager), channelManager = new ChannelManager(API, API.GetNotificationsConnector())), + (typeof(ChannelManager), channelManager = new ChannelManager(API)), }, Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs index cb93812bc2..57514cdf37 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Online public TestContainer(IAPIProvider api, Channel[] channels) { this.channels = channels; - ChannelManager = new ChannelManager(api, api.GetNotificationsConnector()); + ChannelManager = new ChannelManager(api); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 07196cc92a..34ecad7dc1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online { var api = parent.Get(); - Add(channelManager = new ChannelManager(api, api.GetNotificationsConnector())); + Add(channelManager = new ChannelManager(api)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 48ff45974d..ca2b400e8b 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Components if (manager == null) { - AddInternal(manager = new ChannelManager(api, api.GetNotificationsConnector())); + AddInternal(manager = new ChannelManager(api)); Channel.BindTo(manager.CurrentChannel); } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 77d53911d9..a9603506c6 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -74,10 +74,11 @@ namespace osu.Game.Online.Chat private bool channelsInitialised; private ScheduledDelegate ackDelegate; - public ChannelManager(IAPIProvider api, NotificationsClientConnector connector) + public ChannelManager(IAPIProvider api) { this.api = api; - this.connector = connector; + + connector = api.GetNotificationsConnector(); CurrentChannel.ValueChanged += currentChannelChanged; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f36d83d5c2..3a8f202d97 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -44,7 +44,6 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Online.Notifications; using osu.Game.Overlays; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; @@ -84,7 +83,6 @@ namespace osu.Game private ChatOverlay chatOverlay; private ChannelManager channelManager; - private NotificationsClientConnector notificationsClient; [NotNull] protected readonly NotificationOverlay Notifications = new NotificationOverlay(); @@ -680,7 +678,6 @@ namespace osu.Game { base.Dispose(isDisposing); SentryLogger.Dispose(); - notificationsClient.Dispose(); } protected override IDictionary GetFrameworkConfigDefaults() @@ -758,7 +755,6 @@ namespace osu.Game BackButton.Receptor receptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); - dependencies.CacheAs(notificationsClient = API.GetNotificationsConnector()); var sessionIdleTracker = new GameIdleTracker(300000); sessionIdleTracker.IsIdle.BindValueChanged(idle => @@ -885,7 +881,7 @@ namespace osu.Game loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); - loadComponentSingleFile(channelManager = new ChannelManager(API, notificationsClient), AddInternal, true); + loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true); From 66bbe3411628b0b9664e66ec66f15d241843b1bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 18:52:57 +0900 Subject: [PATCH 020/152] Move polling clients to osu.Game.Tests namespace --- osu.Game/Online/API/DummyAPIAccess.cs | 2 +- .../Polling => Tests}/PollingNotificationsClient.cs | 3 ++- .../Polling => Tests}/PollingNotificationsClientConnector.cs | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) rename osu.Game/{Online/Notifications/Polling => Tests}/PollingNotificationsClient.cs (94%) rename osu.Game/{Online/Notifications/Polling => Tests}/PollingNotificationsClientConnector.cs (92%) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 865e1e0a70..609efd8ab6 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -10,7 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Notifications; -using osu.Game.Online.Notifications.Polling; +using osu.Game.Tests; using osu.Game.Users; namespace osu.Game.Online.API diff --git a/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs b/osu.Game/Tests/PollingNotificationsClient.cs similarity index 94% rename from osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs rename to osu.Game/Tests/PollingNotificationsClient.cs index 2f343abf86..c1f032a647 100644 --- a/osu.Game/Online/Notifications/Polling/PollingNotificationsClient.cs +++ b/osu.Game/Tests/PollingNotificationsClient.cs @@ -4,8 +4,9 @@ using System.Threading; using System.Threading.Tasks; using osu.Game.Online.API; +using osu.Game.Online.Notifications; -namespace osu.Game.Online.Notifications.Polling +namespace osu.Game.Tests { /// /// A notifications client which polls for new messages every second. diff --git a/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs b/osu.Game/Tests/PollingNotificationsClientConnector.cs similarity index 92% rename from osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs rename to osu.Game/Tests/PollingNotificationsClientConnector.cs index ff3f30edcd..823fc9d157 100644 --- a/osu.Game/Online/Notifications/Polling/PollingNotificationsClientConnector.cs +++ b/osu.Game/Tests/PollingNotificationsClientConnector.cs @@ -4,8 +4,9 @@ using System.Threading; using System.Threading.Tasks; using osu.Game.Online.API; +using osu.Game.Online.Notifications; -namespace osu.Game.Online.Notifications.Polling +namespace osu.Game.Tests { /// /// A connector for s that poll for new messages. From 1d2818dc70dbafecb5a60db70c9229917bdcc487 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 19:02:26 +0900 Subject: [PATCH 021/152] Reschedule ack request on completion --- osu.Game/Online/Chat/ChannelManager.cs | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index a9603506c6..035957a7c4 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -72,7 +72,7 @@ namespace osu.Game.Online.Chat private readonly IBindable apiState = new Bindable(); private bool channelsInitialised; - private ScheduledDelegate ackDelegate; + private ScheduledDelegate scheduledAck; public ChannelManager(IAPIProvider api) { @@ -112,16 +112,28 @@ namespace osu.Game.Online.Chat }); apiState.BindTo(api.State); - apiState.BindValueChanged(status => - { - ackDelegate?.Cancel(); + apiState.BindValueChanged(_ => performChatAckRequest(), true); + } - if (status.NewValue == APIState.Online) - { - Scheduler.Add(ackDelegate = new ScheduledDelegate(() => api.Queue(new ChatAckRequest()), 0, 60000)); - // Todo: Handle silences. - } - }, true); + private void performChatAckRequest() + { + if (apiState.Value != APIState.Online) + return; + + scheduledAck?.Cancel(); + + var req = new ChatAckRequest(); + req.Success += _ => scheduleNextRequest(); + req.Failure += _ => scheduleNextRequest(); + api.Queue(req); + + // Todo: Handle silences. + + void scheduleNextRequest() + { + scheduledAck?.Cancel(); + scheduledAck = Scheduler.AddDelayed(performChatAckRequest, 60000); + } } /// From f12ada9d9247db0fee8cad42d7418bef011db179 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 19:36:24 +0900 Subject: [PATCH 022/152] Fix chat connecting too early --- osu.Game/Online/Chat/ChannelManager.cs | 2 ++ osu.Game/Online/HubClientConnector.cs | 3 +++ .../Online/PersistentEndpointClientConnector.cs | 13 ++++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 035957a7c4..db2436333b 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -111,6 +111,8 @@ namespace osu.Game.Online.Chat } }); + connector.Start(); + apiState.BindTo(api.State); apiState.BindValueChanged(_ => performChatAckRequest(), true); } diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 6f246f6dd3..ca6d2932f7 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -49,6 +49,9 @@ namespace osu.Game.Online this.api = api; this.versionHash = versionHash; this.preferMessagePack = preferMessagePack; + + // Automatically start these connections. + Start(); } protected override Task BuildConnectionAsync(CancellationToken cancellationToken) diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index 11e8e870d6..2c4e127723 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -29,6 +29,7 @@ namespace osu.Game.Online private readonly Bindable isConnected = new Bindable(); private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); + private bool started; /// /// Constructs a new . @@ -37,9 +38,19 @@ namespace osu.Game.Online protected PersistentEndpointClientConnector(IAPIProvider api) { API = api; - apiState.BindTo(api.State); + } + + /// + /// Attempts to connect and begins processing messages from the remote endpoint. + /// + public void Start() + { + if (started) + return; + apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true); + started = true; } public Task Reconnect() From d426977f038f0e83e9c15ca952433f51ac654a02 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Nov 2022 20:11:42 +0900 Subject: [PATCH 023/152] Handle channel joins --- .../WebSocket/WebSocketNotificationsClient.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index bc4ceea993..eefe684795 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -113,6 +113,15 @@ namespace osu.Game.Online.Notifications.WebSocket { switch (message.Event) { + case @"chat.channel.join": + Debug.Assert(message.Data != null); + + Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString()); + Debug.Assert(joinedChannel != null); + + HandleJoinedChannel(joinedChannel); + break; + case @"chat.message.new": Debug.Assert(message.Data != null); From b7ef9b176d3c9c0de0e116c34c73e329acc18d3f Mon Sep 17 00:00:00 2001 From: maromalo <54760464+maromalo@users.noreply.github.com> Date: Sun, 6 Nov 2022 19:59:27 -0300 Subject: [PATCH 024/152] Make score type consistent --- .../TestSceneSpinnerRotation.cs | 2 +- .../Gameplay/TestSceneScoreProcessor.cs | 2 +- .../Gameplay/TestSceneGameplayLeaderboard.cs | 10 +++++----- .../Graphics/UserInterface/ScoreCounter.cs | 6 +++--- .../API/Requests/Responses/SoloScoreInfo.cs | 4 ++-- .../Online/Rooms/IndexedMultiplayerScores.cs | 2 +- .../Spectator/SpectatorScoreProcessor.cs | 2 +- .../PerformanceBreakdownCalculator.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 18 +++++++++--------- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Scoring/ScoringValues.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../Play/HUD/GameplayLeaderboardScore.cs | 2 +- osu.Game/Screens/Play/HUD/ILeaderboardScore.cs | 2 +- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 2 +- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 5fa4e24f5e..13ea46eadf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("player score matching expected bonus score", () => { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) - double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; + long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index fb9d841d99..d5219f6391 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Gameplay // Apply a miss judgement scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement()) { Type = HitResult.Miss }); - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0)); + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0)); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 171ae829a9..30d2cc6423 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay { private TestGameplayLeaderboard leaderboard; - private readonly BindableDouble playerScore = new BindableDouble(); + private readonly BindableLong playerScore = new BindableLong(); public TestSceneGameplayLeaderboard() { @@ -76,8 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay createLeaderboard(); addLocalPlayer(); - var player2Score = new BindableDouble(1234567); - var player3Score = new BindableDouble(1111111); + var player2Score = new BindableLong(1234567); + var player3Score = new BindableLong(1111111); AddStep("add player 2", () => createLeaderboardScore(player2Score, new APIUser { Username = "Player 2" })); AddStep("add player 3", () => createLeaderboardScore(player3Score, new APIUser { Username = "Player 3" })); @@ -161,9 +161,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user); + private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableLong(RNG.Next(0, 5_000_000)), user); - private void createLeaderboardScore(BindableDouble score, APIUser user, bool isTracked = false) + private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) { var leaderboardScore = leaderboard.Add(user, isTracked); leaderboardScore.TotalScore.BindTo(score); diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index e46e2b31ac..2efe27c842 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { - public abstract class ScoreCounter : RollingCounter + public abstract class ScoreCounter : RollingCounter { protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; @@ -36,10 +36,10 @@ namespace osu.Game.Graphics.UserInterface UpdateDisplay(); } - protected override double GetProportionalDuration(double currentValue, double newValue) => + protected override double GetProportionalDuration(long currentValue, long newValue) => currentValue > newValue ? currentValue - newValue : newValue - currentValue; - protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString); + protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(formatString); protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 77dcfd39e3..15f4bace96 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -31,7 +31,7 @@ namespace osu.Game.Online.API.Requests.Responses public bool Passed { get; set; } [JsonProperty("total_score")] - public int TotalScore { get; set; } + public long TotalScore { get; set; } [JsonProperty("accuracy")] public double Accuracy { get; set; } @@ -213,7 +213,7 @@ namespace osu.Game.Online.API.Requests.Responses public static SoloScoreInfo ForSubmission(ScoreInfo score) => new SoloScoreInfo { Rank = score.Rank, - TotalScore = (int)score.TotalScore, + TotalScore = score.TotalScore, Accuracy = score.Accuracy, PP = score.PP, MaxCombo = score.MaxCombo, diff --git a/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs index 459602f1b4..59cba2340d 100644 --- a/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs +++ b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs @@ -17,7 +17,7 @@ namespace osu.Game.Online.Rooms /// The total scores in the playlist item. /// [JsonProperty("total")] - public int? TotalScores { get; set; } + public long? TotalScores { get; set; } /// /// The user's score, if any. diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs index 87f25874c5..573c504add 100644 --- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs +++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs @@ -26,7 +26,7 @@ namespace osu.Game.Online.Spectator /// /// The current total score. /// - public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 }; + public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 }; /// /// The current accuracy. diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 3fb12041d1..4f802a22a1 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty // calculate total score ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = perfectPlay.Mods; - perfectPlay.TotalScore = (long)scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); + perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); // compute rank achieved // default to SS, then adjust the rank with mods diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7456ce06bd..f619c89368 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The current total score. /// - public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 }; + public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 }; /// /// The current accuracy. @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { - Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; + Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues); } @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Scoring /// The to compute the total score of. /// The total score in the given . [Pure] - public double ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) + public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) { if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); @@ -316,9 +316,9 @@ namespace osu.Game.Rulesets.Scoring /// The maximum scoring values. /// The total score computed from the given scoring values. [Pure] - public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum) + public long ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum) { - double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1; + double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1; double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1; return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects); } @@ -333,7 +333,7 @@ namespace osu.Game.Rulesets.Scoring /// The total number of basic (non-tick and non-bonus) hitobjects in the beatmap. /// The total score computed from the given scoring component ratios. [Pure] - public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects) + public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects) { switch (mode) { @@ -341,13 +341,13 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + return (long)((max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier); case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score; - return Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier; + return (long)(Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); } } @@ -417,7 +417,7 @@ namespace osu.Game.Rulesets.Scoring score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. - score.TotalScore = (long)Math.Round(ComputeScore(ScoringMode.Standardised, score)); + score.TotalScore = ComputeScore(ScoringMode.Standardised, score); } /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 8342d3bcc1..b2944ad219 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -99,7 +99,7 @@ namespace osu.Game.Scoring var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - return (long)Math.Round(scoreProcessor.ComputeScore(mode, score)); + return scoreProcessor.ComputeScore(mode, score); } /// diff --git a/osu.Game/Scoring/ScoringValues.cs b/osu.Game/Scoring/ScoringValues.cs index 9bc4e6e12a..471067c9db 100644 --- a/osu.Game/Scoring/ScoringValues.cs +++ b/osu.Game/Scoring/ScoringValues.cs @@ -20,13 +20,13 @@ namespace osu.Game.Scoring /// The sum of all "basic" scoring values. See: and . /// [Key(0)] - public double BaseScore; + public long BaseScore; /// /// The sum of all "bonus" scoring values. See: and . /// [Key(1)] - public double BonusScore; + public long BonusScore; /// /// The highest achieved combo. diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 1c4d02bb11..2444729118 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo)); + Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 2eec8253b3..15586baab0 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; - public BindableDouble TotalScore { get; } = new BindableDouble(); + public BindableLong TotalScore { get; } = new BindableLong(); public BindableDouble Accuracy { get; } = new BindableDouble(1); public BindableInt Combo { get; } = new BindableInt(); public BindableBool HasQuit { get; } = new BindableBool(); diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index aa06bb08a5..428390f90c 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.Play.HUD { public interface ILeaderboardScore { - BindableDouble TotalScore { get; } + BindableLong TotalScore { get; } BindableDouble Accuracy { get; } BindableInt Combo { get; } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 4201b3f4c9..a77d66cdd2 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD continue; if (TeamScores.TryGetValue(u.Team.Value, out var team)) - team.Value += (int)Math.Round(u.ScoreProcessor.TotalScore.Value); + team.Value += u.ScoreProcessor.TotalScore.Value; } } From 8eef2ba8de2bfdce8f4c4278f0390259ab46587b Mon Sep 17 00:00:00 2001 From: maromalo <54760464+maromalo@users.noreply.github.com> Date: Sun, 6 Nov 2022 20:12:26 -0300 Subject: [PATCH 025/152] quality shenanigans --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a77d66cdd2..4ac92056ef 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -1,7 +1,6 @@ // 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.Collections.Specialized; using System.Linq; From e3adf5a98502d714c6849095f257a2ad343b5038 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Nov 2022 11:36:55 +0900 Subject: [PATCH 026/152] Handle channel parts --- osu.Game/Online/Chat/ChannelManager.cs | 2 ++ osu.Game/Online/Notifications/NotificationsClient.cs | 3 +++ .../Online/Notifications/NotificationsClientConnector.cs | 2 ++ .../WebSocket/WebSocketNotificationsClient.cs | 9 +++++++++ 4 files changed, 16 insertions(+) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index db2436333b..93033dffa0 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -99,6 +99,8 @@ namespace osu.Game.Online.Chat joinChannel(localChannel); }); + connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch))); + connector.NewMessages += msgs => Schedule(() => addMessages(msgs)); connector.PresenceReceived += () => Schedule(() => diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index cb5e94fb1d..5182bfa0e5 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -18,6 +18,7 @@ namespace osu.Game.Online.Notifications public abstract class NotificationsClient : PersistentEndpointClient { public Action? ChannelJoined; + public Action? ChannelParted; public Action>? NewMessages; public Action? PresenceReceived; @@ -65,6 +66,8 @@ namespace osu.Game.Online.Notifications ChannelJoined?.Invoke(channel); } + protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel); + protected void HandleMessages(List messages) { NewMessages?.Invoke(messages); diff --git a/osu.Game/Online/Notifications/NotificationsClientConnector.cs b/osu.Game/Online/Notifications/NotificationsClientConnector.cs index d4c846a7a2..d2c2e6673c 100644 --- a/osu.Game/Online/Notifications/NotificationsClientConnector.cs +++ b/osu.Game/Online/Notifications/NotificationsClientConnector.cs @@ -16,6 +16,7 @@ namespace osu.Game.Online.Notifications public abstract class NotificationsClientConnector : PersistentEndpointClientConnector { public event Action? ChannelJoined; + public event Action? ChannelParted; public event Action>? NewMessages; public event Action? PresenceReceived; @@ -29,6 +30,7 @@ namespace osu.Game.Online.Notifications var client = await BuildNotificationClientAsync(cancellationToken); client.ChannelJoined = c => ChannelJoined?.Invoke(c); + client.ChannelParted = c => ChannelParted?.Invoke(c); client.NewMessages = m => NewMessages?.Invoke(m); client.PresenceReceived = () => PresenceReceived?.Invoke(); diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index eefe684795..8f4b3c2f97 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -122,6 +122,15 @@ namespace osu.Game.Online.Notifications.WebSocket HandleJoinedChannel(joinedChannel); break; + case @"chat.channel.part": + Debug.Assert(message.Data != null); + + Channel? partedChannel = JsonConvert.DeserializeObject(message.Data.ToString()); + Debug.Assert(partedChannel != null); + + HandleChannelParted(partedChannel); + break; + case @"chat.message.new": Debug.Assert(message.Data != null); From cf03001c83a7f6317513fd283a7761427c050e72 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Nov 2022 11:52:07 +0900 Subject: [PATCH 027/152] Better handling for joining channels with only ID --- .../Online/API/Requests/GetChannelRequest.cs | 19 +++++++++++++ .../Requests/Responses/GetChannelResponse.cs | 19 +++++++++++++ osu.Game/Online/Chat/ChannelManager.cs | 28 +++++-------------- .../Notifications/NotificationsClient.cs | 10 +++---- .../WebSocket/WebSocketNotificationsClient.cs | 3 +- 5 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Online/API/Requests/GetChannelRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs diff --git a/osu.Game/Online/API/Requests/GetChannelRequest.cs b/osu.Game/Online/API/Requests/GetChannelRequest.cs new file mode 100644 index 0000000000..5bc9cb519a --- /dev/null +++ b/osu.Game/Online/API/Requests/GetChannelRequest.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetChannelRequest : APIRequest + { + private readonly long channelId; + + public GetChannelRequest(long channelId) + { + this.channelId = channelId; + } + + protected override string Target => $"chat/channels/{channelId}"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs b/osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs new file mode 100644 index 0000000000..24b886e74d --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/GetChannelResponse.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetChannelResponse + { + [JsonProperty(@"channel")] + public Channel Channel { get; set; } = null!; + + [JsonProperty(@"users")] + public List Users { get; set; } = null!; + } +} diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 93033dffa0..ee3194243b 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -88,15 +88,14 @@ namespace osu.Game.Online.Chat { connector.ChannelJoined += ch => Schedule(() => { - var localChannel = getChannel(ch); - - if (localChannel != ch) + if (ch.Joined.Value) + JoinChannel(ch); + else { - localChannel.Joined.Value = true; - localChannel.Id = ch.Id; + var req = new GetChannelRequest(ch.Id); + req.Success += response => JoinChannel(response.Channel); + api.Queue(req); } - - joinChannel(localChannel); }); connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch))); @@ -421,20 +420,7 @@ namespace osu.Game.Online.Chat { Channel found = null; - bool lookupCondition(Channel ch) - { - // If both channels have an id, use that. - if (lookup.Id > 0 && ch.Id > 0) - return ch.Id == lookup.Id; - - // In the case that the local echo is received in a new channel (i.e. one that does not yet have an ID), - // then we need to check for any existing channel with the message containing the same message matched by UUID. - if (lookup.Messages.Count > 0 && ch.Messages.Any(m => m.Uuid == lookup.Messages.Last().Uuid)) - return true; - - // As a last resort, fallback to matching by name. - return lookup.Name == ch.Name; - } + bool lookupCondition(Channel ch) => lookup.Id > 0 ? ch.Id == lookup.Id : ch.Name == lookup.Name; var available = AvailableChannels.FirstOrDefault(lookupCondition); if (available != null) diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index 5182bfa0e5..c706124351 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -46,7 +46,10 @@ namespace osu.Game.Online.Notifications if (updates?.Presence != null) { foreach (var channel in updates.Presence) + { + channel.Joined.Value = true; HandleJoinedChannel(channel); + } //todo: handle left channels @@ -59,12 +62,7 @@ namespace osu.Game.Online.Notifications return fetchReq; } - protected void HandleJoinedChannel(Channel channel) - { - // we received this from the server so should mark the channel already joined. - channel.Joined.Value = true; - ChannelJoined?.Invoke(channel); - } + protected void HandleJoinedChannel(Channel channel) => ChannelJoined?.Invoke(channel); protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel); diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index 8f4b3c2f97..71aec942ac 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -119,6 +119,7 @@ namespace osu.Game.Online.Notifications.WebSocket Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString()); Debug.Assert(joinedChannel != null); + joinedChannel.Joined.Value = true; HandleJoinedChannel(joinedChannel); break; @@ -138,7 +139,7 @@ namespace osu.Game.Online.Notifications.WebSocket Debug.Assert(messageData != null); foreach (var msg in messageData.Messages) - HandleJoinedChannel(new Channel(msg.Sender) { Id = msg.ChannelId, Messages = { msg } }); + HandleJoinedChannel(new Channel { Id = msg.ChannelId }); HandleMessages(messageData.Messages); break; From cd8402df72f3945f0b0b56e55691e4117badb282 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Nov 2022 12:11:44 +0900 Subject: [PATCH 028/152] Print event type to logs --- .../Notifications/WebSocket/WebSocketNotificationsClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index 71aec942ac..c5b11ea94a 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -69,13 +69,14 @@ namespace osu.Game.Online.Notifications.WebSocket break; } + Logger.Log($"{GetType().ReadableName()} handling event: {message.Event}"); await onMessageReceivedAsync(message); } break; case WebSocketMessageType.Binary: - throw new NotImplementedException(); + throw new NotImplementedException("Binary message type not supported."); case WebSocketMessageType.Close: throw new Exception("Connection closed by remote host."); From f931bdc5ff502518a29e7714507245c833b307cc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Nov 2022 12:25:23 +0900 Subject: [PATCH 029/152] Fix channel lookup not considering missing ids --- osu.Game/Online/Chat/ChannelManager.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index ee3194243b..ee6e22fbc1 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -420,7 +420,13 @@ namespace osu.Game.Online.Chat { Channel found = null; - bool lookupCondition(Channel ch) => lookup.Id > 0 ? ch.Id == lookup.Id : ch.Name == lookup.Name; + bool lookupCondition(Channel ch) + { + if (ch.Id > 0 && lookup.Id > 0) + return ch.Id == lookup.Id; + + return ch.Name == lookup.Name; + } var available = AvailableChannels.FirstOrDefault(lookupCondition); if (available != null) From 67e99b53448c9311e9aa65543aa94aaf5db031eb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Nov 2022 14:34:53 +0900 Subject: [PATCH 030/152] Lookup channels before calling HandleJoinedChannel() --- osu.Game/Online/Chat/ChannelManager.cs | 18 +++++------- .../Notifications/NotificationsClient.cs | 9 +++--- .../WebSocket/WebSocketNotificationsClient.cs | 29 ++++++++++++++++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index ee6e22fbc1..076f79a700 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -86,17 +86,7 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load() { - connector.ChannelJoined += ch => Schedule(() => - { - if (ch.Joined.Value) - JoinChannel(ch); - else - { - var req = new GetChannelRequest(ch.Id); - req.Success += response => JoinChannel(response.Channel); - api.Queue(req); - } - }); + connector.ChannelJoined += ch => Schedule(() => joinChannel(ch)); connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch))); @@ -446,6 +436,12 @@ namespace osu.Game.Online.Chat if (foundSelf != null) found.Users.Remove(foundSelf); } + else + { + found.Id = lookup.Id; + found.Name = lookup.Name; + found.LastMessageId = Math.Max(found.LastMessageId ?? 0, lookup.LastMessageId ?? 0); + } if (joined == null && addToJoined) joinedChannels.Add(found); if (available == null && addToAvailable) availableChannels.Add(found); diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index c706124351..e5f477bc1e 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -46,10 +46,7 @@ namespace osu.Game.Online.Notifications if (updates?.Presence != null) { foreach (var channel in updates.Presence) - { - channel.Joined.Value = true; HandleJoinedChannel(channel); - } //todo: handle left channels @@ -62,7 +59,11 @@ namespace osu.Game.Online.Notifications return fetchReq; } - protected void HandleJoinedChannel(Channel channel) => ChannelJoined?.Invoke(channel); + protected void HandleJoinedChannel(Channel channel) + { + channel.Joined.Value = true; + ChannelJoined?.Invoke(channel); + } protected void HandleChannelParted(Channel channel) => ChannelParted?.Invoke(channel); diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index c5b11ea94a..788847d368 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.Net.WebSockets; using System.Text; @@ -11,6 +12,7 @@ using Newtonsoft.Json; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Logging; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; namespace osu.Game.Online.Notifications.WebSocket @@ -22,6 +24,7 @@ namespace osu.Game.Online.Notifications.WebSocket { private readonly ClientWebSocket socket; private readonly string endpoint; + private readonly ConcurrentDictionary channelsMap = new ConcurrentDictionary(); public WebSocketNotificationsClient(ClientWebSocket socket, string endpoint, IAPIProvider api) : base(api) @@ -110,7 +113,7 @@ namespace osu.Game.Online.Notifications.WebSocket await socket.SendAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), WebSocketMessageType.Text, true, cancellationToken); } - private Task onMessageReceivedAsync(SocketMessage message) + private async Task onMessageReceivedAsync(SocketMessage message) { switch (message.Event) { @@ -120,7 +123,6 @@ namespace osu.Game.Online.Notifications.WebSocket Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString()); Debug.Assert(joinedChannel != null); - joinedChannel.Joined.Value = true; HandleJoinedChannel(joinedChannel); break; @@ -140,13 +142,32 @@ namespace osu.Game.Online.Notifications.WebSocket Debug.Assert(messageData != null); foreach (var msg in messageData.Messages) - HandleJoinedChannel(new Channel { Id = msg.ChannelId }); + HandleJoinedChannel(await getChannel(msg.ChannelId)); HandleMessages(messageData.Messages); break; } + } - return Task.CompletedTask; + private async Task getChannel(long channelId) + { + if (channelsMap.TryGetValue(channelId, out Channel channel)) + return channel; + + var tsc = new TaskCompletionSource(); + var req = new GetChannelRequest(channelId); + + req.Success += response => + { + channelsMap[channelId] = response.Channel; + tsc.SetResult(response.Channel); + }; + + req.Failure += ex => tsc.SetException(ex); + + API.Queue(req); + + return await tsc.Task; } public override async ValueTask DisposeAsync() From fdca3c2d1c5e6d122e297bd4204b2115f081eb13 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Nov 2022 14:35:42 +0900 Subject: [PATCH 031/152] Rename method for consistency --- osu.Game/Online/Notifications/NotificationsClient.cs | 4 ++-- .../Notifications/WebSocket/WebSocketNotificationsClient.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Notifications/NotificationsClient.cs b/osu.Game/Online/Notifications/NotificationsClient.cs index e5f477bc1e..6198707111 100644 --- a/osu.Game/Online/Notifications/NotificationsClient.cs +++ b/osu.Game/Online/Notifications/NotificationsClient.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Notifications if (updates?.Presence != null) { foreach (var channel in updates.Presence) - HandleJoinedChannel(channel); + HandleChannelJoined(channel); //todo: handle left channels @@ -59,7 +59,7 @@ namespace osu.Game.Online.Notifications return fetchReq; } - protected void HandleJoinedChannel(Channel channel) + protected void HandleChannelJoined(Channel channel) { channel.Joined.Value = true; ChannelJoined?.Invoke(channel); diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index 788847d368..86836099d8 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -123,7 +123,7 @@ namespace osu.Game.Online.Notifications.WebSocket Channel? joinedChannel = JsonConvert.DeserializeObject(message.Data.ToString()); Debug.Assert(joinedChannel != null); - HandleJoinedChannel(joinedChannel); + HandleChannelJoined(joinedChannel); break; case @"chat.channel.part": @@ -142,7 +142,7 @@ namespace osu.Game.Online.Notifications.WebSocket Debug.Assert(messageData != null); foreach (var msg in messageData.Messages) - HandleJoinedChannel(await getChannel(msg.ChannelId)); + HandleChannelJoined(await getChannel(msg.ChannelId)); HandleMessages(messageData.Messages); break; From ddc2ed1542a254c619692595ac1ef7330523dc40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 13:22:01 +0900 Subject: [PATCH 032/152] Fix height of playfield in taiko tests --- .../Skinning/TestSceneDrawableBarLine.cs | 6 ++--- .../Skinning/TestSceneTaikoPlayfield.cs | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index ad6f08dbd4..a4aa0e1fad 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning var cont = new Container { RelativeSizeAxes = Axes.Both, - Height = 0.8f, + Height = 0.2f, Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new Drawable[] @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning var cont = new Container { RelativeSizeAxes = Axes.Both, - Height = 0.8f, + Height = 0.2f, Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new Drawable[] @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning var barLine = new BarLine { Major = major, - StartTime = Time.Current + 2000, + StartTime = Time.Current + 5000, }; var cpi = new ControlPointInfo(); diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index 52d24b567f..eff9f58751 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -25,11 +26,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; - public TestSceneTaikoPlayfield() + [SetUpSteps] + public void SetUpSteps() { TaikoBeatmap beatmap; - bool kiai = false; - AddStep("set beatmap", () => { Beatmap.Value = CreateWorkingBeatmap(beatmap = new TaikoBeatmap()); @@ -41,12 +41,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield { + Height = 0.2f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Height = 0.6f, })); + } + [Test] + public void TestBasic() + { + AddStep("do nothing", () => { }); + } + + [Test] + public void TestHeightChanges() + { AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50); + } + + [Test] + public void TestKiai() + { + bool kiai = false; AddStep("Toggle kiai", () => { From baf8db8de4ae9a2b26d9401b02b66463ffd39cae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Nov 2022 15:45:32 +0900 Subject: [PATCH 033/152] Add basic setup for taiko argon skinning --- .../Argon/TaikoArgonSkinTransformer.cs | 34 +++++++++++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 4 +++ 2 files changed, 38 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs new file mode 100644 index 0000000000..13c484af24 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class TaikoArgonSkinTransformer : SkinTransformer + { + public TaikoArgonSkinTransformer(ISkin skin) + : base(skin) + { + } + + public override Drawable? GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case TaikoSkinComponent catchComponent: + // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. + switch (catchComponent.Component) + { + case TaikoSkinComponents.CentreHit: + return Drawable.Empty(); + } + + break; + } + + return base.GetDrawableComponent(component); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index dc36bc0320..fe12cf9765 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Rulesets.Taiko.Scoring; +using osu.Game.Rulesets.Taiko.Skinning.Argon; using osu.Game.Rulesets.Taiko.Skinning.Legacy; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; @@ -47,6 +48,9 @@ namespace osu.Game.Rulesets.Taiko { switch (skin) { + case ArgonSkin: + return new TaikoArgonSkinTransformer(skin); + case LegacySkin: return new TaikoLegacySkinTransformer(skin); } From bc0e9375afcc54b108246f6bb60e2b3e50425b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Nov 2022 16:52:09 +0900 Subject: [PATCH 034/152] Add basic argon hits --- .../Skinning/Argon/ArgonCentreCirclePiece.cs | 21 ++++ .../Skinning/Argon/ArgonCirclePiece.cs | 116 ++++++++++++++++++ .../Skinning/Argon/ArgonRimCirclePiece.cs | 21 ++++ .../Skinning/Argon/RingPiece.cs | 40 ++++++ .../Argon/TaikoArgonSkinTransformer.cs | 5 +- 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs new file mode 100644 index 0000000000..234d9f659a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Colour; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonCentreCirclePiece : ArgonCirclePiece + { + [BackgroundDependencyLoader] + private void load() + { + AccentColour = ColourInfo.GradientVertical( + new Color4(241, 0, 0, 255), + new Color4(167, 0, 0, 255) + ); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs new file mode 100644 index 0000000000..b58b7455fd --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public abstract class ArgonCirclePiece : BeatSyncedContainer + { + private const double pre_beat_transition_time = 80; + + private const float flash_opacity = 0.3f; + + private ColourInfo accentColour; + + /// + /// The colour of the inner circle and outer glows. + /// + public ColourInfo AccentColour + { + get => accentColour; + set + { + accentColour = value; + ring.Colour = AccentColour.MultiplyAlpha(0.5f); + ring2.Colour = AccentColour; + } + } + + /// + /// Whether Kiai mode effects are enabled for this circle piece. + /// + public bool KiaiMode { get; set; } + + public Box FlashBox; + + private readonly RingPiece ring; + private readonly RingPiece ring2; + + protected ArgonCirclePiece() + { + RelativeSizeAxes = Axes.Both; + + EarlyActivationMilliseconds = pre_beat_transition_time; + + AddRangeInternal(new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 22, 30, 190) + }, + ring = new RingPiece(20 / 70f), + ring2 = new RingPiece(5 / 70f), + new CircularContainer + { + Name = "Flash layer", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] + { + FlashBox = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + AlwaysPresent = true + } + }, + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.AngleLeft, + Size = new Vector2(20 / 70f), + Scale = new Vector2(0.8f, 1) + } + }); + } + + [Resolved] + private DrawableHitObject drawableHitObject { get; set; } = null!; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + if (!effectPoint.KiaiMode) + return; + + if (drawableHitObject.State.Value == ArmedState.Idle) + { + FlashBox + .FadeTo(flash_opacity) + .Then() + .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs new file mode 100644 index 0000000000..5dc955c56e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Colour; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonRimCirclePiece : ArgonCirclePiece + { + [BackgroundDependencyLoader] + private void load() + { + AccentColour = ColourInfo.GradientVertical( + new Color4(0, 161, 241, 255), + new Color4(0, 111, 167, 255) + ); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs new file mode 100644 index 0000000000..2c5d824ff5 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class RingPiece : CircularContainer + { + private readonly float relativeBorderThickness; + + public RingPiece(float relativeBorderThickness) + { + this.relativeBorderThickness = relativeBorderThickness; + RelativeSizeAxes = Axes.Both; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Masking = true; + BorderColour = Color4.White; + + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + }; + } + + protected override void Update() + { + base.Update(); + BorderThickness = relativeBorderThickness * DrawSize.X; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 13c484af24..f223d35b0a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -22,7 +22,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon switch (catchComponent.Component) { case TaikoSkinComponents.CentreHit: - return Drawable.Empty(); + return new ArgonCentreCirclePiece(); + + case TaikoSkinComponents.RimHit: + return new ArgonRimCirclePiece(); } break; From 421bdd2c1a3736c0583150deb5c585f8d64771a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 13:16:51 +0900 Subject: [PATCH 035/152] Add playfield background implementations --- .../Argon/ArgonPlayfieldBackgroundLeft.cs | 27 ++++++++++++++++++ .../Argon/ArgonPlayfieldBackgroundRight.cs | 28 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 6 ++++ 3 files changed, 61 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs new file mode 100644 index 0000000000..ebde83b607 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundLeft.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonPlayfieldBackgroundLeft : CompositeDrawable + { + public ArgonPlayfieldBackgroundLeft() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs new file mode 100644 index 0000000000..bd0f3ab276 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonPlayfieldBackgroundRight.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonPlayfieldBackgroundRight : CompositeDrawable + { + public ArgonPlayfieldBackgroundRight() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Black, + Alpha = 0.7f, + RelativeSizeAxes = Axes.Both, + }, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index f223d35b0a..d2a3e19f38 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -26,6 +26,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.RimHit: return new ArgonRimCirclePiece(); + + case TaikoSkinComponents.PlayfieldBackgroundLeft: + return new ArgonPlayfieldBackgroundLeft(); + + case TaikoSkinComponents.PlayfieldBackgroundRight: + return new ArgonPlayfieldBackgroundRight(); } break; From f1a1f29da78de9d49de56b9fda3728ccd84453b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 13:43:04 +0900 Subject: [PATCH 036/152] Add hit target implementation --- .../Skinning/Argon/ArgonHitTarget.cs | 72 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 3 + 2 files changed, 75 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs new file mode 100644 index 0000000000..ec2eccd595 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitTarget.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Taiko.Objects; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonHitTarget : CompositeDrawable + { + /// + /// Thickness of all drawn line pieces. + /// + public ArgonHitTarget() + { + RelativeSizeAxes = Axes.Both; + Masking = true; + + const float border_thickness = 4f; + + InternalChildren = new Drawable[] + { + new Circle + { + Name = "Bar Upper", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = -border_thickness, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE)), + }, + new Circle + { + Name = "Outer circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0.1f, + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE), + Masking = true, + }, + new Circle + { + Name = "Inner circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0.1f, + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * 0.85f), + Masking = true, + }, + new Circle + { + Name = "Bar Lower", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Y, + Y = border_thickness, + Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE)), + }, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index d2a3e19f38..eec7e92511 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -32,6 +32,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.PlayfieldBackgroundRight: return new ArgonPlayfieldBackgroundRight(); + + case TaikoSkinComponents.HitTarget: + return new ArgonHitTarget(); } break; From 529e3217cfea2571d4f405836a592a988fb16a3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 14:01:18 +0900 Subject: [PATCH 037/152] Add barline implementation --- .../Skinning/Argon/ArgonBarLine.cs | 83 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 3 + 2 files changed, 86 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs new file mode 100644 index 0000000000..402e88b64d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonBarLine.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonBarLine : CompositeDrawable + { + private Container majorEdgeContainer = null!; + + private Bindable major = null!; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject) + { + RelativeSizeAxes = Axes.Both; + + const float line_offset = 8; + var majorPieceSize = new Vector2(6, 20); + + InternalChildren = new Drawable[] + { + line = new Box + { + RelativeSizeAxes = Axes.Both, + EdgeSmoothness = new Vector2(0.5f, 0), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + majorEdgeContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Circle + { + Name = "Top line", + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Size = majorPieceSize, + Y = -line_offset, + }, + new Circle + { + Name = "Bottom line", + Anchor = Anchor.BottomCentre, + Origin = Anchor.TopCentre, + Size = majorPieceSize, + Y = line_offset, + }, + } + } + }; + + major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + major.BindValueChanged(updateMajor, true); + } + + private Box line = null!; + + private void updateMajor(ValueChangedEvent major) + { + line.Alpha = major.NewValue ? 1f : 0.5f; + line.Width = major.NewValue ? 1 : 0.5f; + majorEdgeContainer.Alpha = major.NewValue ? 1 : 0; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index eec7e92511..0d5c19a525 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -35,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.HitTarget: return new ArgonHitTarget(); + + case TaikoSkinComponents.BarLine: + return new ArgonBarLine(); } break; From 66365451952639040590a7d328ce4bb8ea9853a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 15:07:33 +0900 Subject: [PATCH 038/152] Move taiko argon hit icon to respective centre/rim pieces --- .../Skinning/Argon/ArgonCentreCirclePiece.cs | 13 +++++++++++++ .../Skinning/Argon/ArgonCirclePiece.cs | 11 ----------- .../Skinning/Argon/ArgonRimCirclePiece.cs | 13 +++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs index 234d9f659a..f347863be9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Argon @@ -16,6 +19,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon new Color4(241, 0, 0, 255), new Color4(167, 0, 0, 255) ); + + AddInternal(new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.AngleLeft, + Size = new Vector2(20 / 70f), + Scale = new Vector2(0.8f, 1) + }); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index b58b7455fd..ba8ef12902 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -7,11 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Argon @@ -84,15 +82,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon } }, }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.AngleLeft, - Size = new Vector2(20 / 70f), - Scale = new Vector2(0.8f, 1) - } }); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs index 5dc955c56e..390f134d6f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Argon @@ -16,6 +19,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon new Color4(0, 161, 241, 255), new Color4(0, 111, 167, 255) ); + + AddInternal(new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.AngleLeft, + Size = new Vector2(20 / 70f), + Scale = new Vector2(0.8f, 1) + }); } } } From f1556c98e3e4ccf5fe89e92f62a8f109b73db730 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 15:07:47 +0900 Subject: [PATCH 039/152] Add drum roll implementation --- .../Argon/ArgonElongatedCirclePiece.cs | 33 +++++++++++++++++++ .../Skinning/Argon/RingPiece.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 3 ++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs new file mode 100644 index 0000000000..f86f181b2e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonElongatedCirclePiece.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonElongatedCirclePiece : ArgonCirclePiece + { + public ArgonElongatedCirclePiece() + { + RelativeSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + AccentColour = ColourInfo.GradientVertical( + new Color4(241, 161, 0, 255), + new Color4(167, 111, 0, 255) + ); + } + + protected override void Update() + { + base.Update(); + Width = Parent.DrawSize.X + DrawHeight; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs index 2c5d824ff5..534a1c71a3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/RingPiece.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon protected override void Update() { base.Update(); - BorderThickness = relativeBorderThickness * DrawSize.X; + BorderThickness = relativeBorderThickness * DrawSize.Y; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 0d5c19a525..64f733a969 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.BarLine: return new ArgonBarLine(); + + case TaikoSkinComponents.DrumRollBody: + return new ArgonElongatedCirclePiece(); } break; From 938a8f865b51e2df7b2508f3a53cb15d577292a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 15:11:40 +0900 Subject: [PATCH 040/152] Adjust transform of taiko drum roll ticks to not scale to 0 (looks bad) --- .../Objects/Drawables/DrawableDrumRollTick.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 95b6384274..ed89d0a14e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -74,7 +74,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Hit: - this.ScaleTo(0, 100, Easing.OutQuint); + this.ScaleTo(1.4f, 200, Easing.OutQuint); + this.FadeOut(200, Easing.OutQuint); break; } } From aa61eb8f4bb61f8f51fef5edb669e62913583f02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 15:17:20 +0900 Subject: [PATCH 041/152] Add note about taiko pooling oversight --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index c0c80eaa4a..400c2f40b1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -133,6 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void OnApply() { base.OnApply(); + + // TODO: THIS CANNOT BE HERE, it makes pooling pointless (see https://github.com/ppy/osu/issues/21072). RecreatePieces(); } From e2046791c2aff7e07532016d8a771f5e4d3b1a3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 15:39:24 +0900 Subject: [PATCH 042/152] Add argon drum roll ticks --- .../Skinning/Argon/ArgonCentreCirclePiece.cs | 2 +- .../Skinning/Argon/ArgonCirclePiece.cs | 2 + .../Skinning/Argon/ArgonRimCirclePiece.cs | 2 +- .../Skinning/Argon/ArgonTickPiece.cs | 68 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 3 + 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs index f347863be9..551a5af078 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCentreCirclePiece.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.AngleLeft, - Size = new Vector2(20 / 70f), + Size = new Vector2(ICON_SIZE), Scale = new Vector2(0.8f, 1) }); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index ba8ef12902..8ef7b71069 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { public abstract class ArgonCirclePiece : BeatSyncedContainer { + public const float ICON_SIZE = 20 / 70f; + private const double pre_beat_transition_time = 80; private const float flash_opacity = 0.3f; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs index 390f134d6f..fd81221be3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonRimCirclePiece.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.AngleLeft, - Size = new Vector2(20 / 70f), + Size = new Vector2(ICON_SIZE), Scale = new Vector2(0.8f, 1) }); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs new file mode 100644 index 0000000000..df63d4948e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonTickPiece.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonTickPiece : CompositeDrawable + { + private readonly Bindable isFirstTick = new Bindable(); + + public ArgonTickPiece() + { + const float tick_size = 1 / TaikoHitObject.DEFAULT_SIZE * ArgonCirclePiece.ICON_SIZE; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + Size = new Vector2(tick_size); + } + + [Resolved] + private DrawableHitObject drawableHitObject { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (drawableHitObject is DrawableDrumRollTick drumRollTick) + isFirstTick.BindTo(drumRollTick.IsFirstTick); + + isFirstTick.BindValueChanged(first => + { + if (first.NewValue) + { + InternalChild = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both + }; + } + else + { + InternalChild = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.AngleLeft, + Scale = new Vector2(0.8f, 1) + }; + } + }, true); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 64f733a969..6782ae67f9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -41,6 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.DrumRollBody: return new ArgonElongatedCirclePiece(); + + case TaikoSkinComponents.DrumRollTick: + return new ArgonTickPiece(); } break; From 37cb187d2eb958167df1397dedda7a53a094f6b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 16:17:43 +0900 Subject: [PATCH 043/152] Move strong hit scale to `DefaultHitExplosion` --- osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs | 2 ++ osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs index b7ba76effa..2e76396a4d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default public void AnimateSecondHit() { + this.ResizeTo(new Vector2(TaikoStrongableHitObject.STRONG_SCALE), 50); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 10a7495c62..d9b6db7734 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -90,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.UI { using (BeginAbsoluteSequence(secondHitTime.Value)) { - this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50); (skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit(); } } From d5c375b139f135e9ffc49ae4c9e67f8979b07865 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 16:18:03 +0900 Subject: [PATCH 044/152] Add argon hit explosion implementation --- .../Skinning/Argon/ArgonHitExplosion.cs | 87 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 9 ++ 2 files changed, 96 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs new file mode 100644 index 0000000000..05bb9bcb9a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion + { + private readonly TaikoSkinComponents component; + private readonly Circle outer; + + public ArgonHitExplosion(TaikoSkinComponents component) + { + this.component = component; + + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + outer = new Circle + { + Name = "Outer circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical( + new Color4(255, 227, 236, 255), + new Color4(255, 198, 211, 255) + ), + Masking = true, + }, + new Circle + { + Name = "Inner circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Size = new Vector2(0.85f), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(255, 132, 191, 255).Opacity(0.5f), + Radius = 45, + }, + Masking = true, + }, + }; + } + + public void Animate(DrawableHitObject drawableHitObject) + { + this.FadeOut(); + + switch (component) + { + case TaikoSkinComponents.TaikoExplosionGreat: + this.FadeIn(30, Easing.In) + .Then() + .FadeOut(450, Easing.OutQuint); + break; + + case TaikoSkinComponents.TaikoExplosionOk: + this.FadeTo(0.2f, 30, Easing.In) + .Then() + .FadeOut(200, Easing.OutQuint); + break; + } + } + + public void AnimateSecondHit() + { + outer.ResizeTo(new Vector2(TaikoStrongableHitObject.STRONG_SCALE), 500, Easing.OutQuint); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 6782ae67f9..3b75cfff11 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -44,6 +44,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.DrumRollTick: return new ArgonTickPiece(); + + case TaikoSkinComponents.TaikoExplosionKiai: + // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. + return Drawable.Empty().With(d => d.Expire()); + + case TaikoSkinComponents.TaikoExplosionGreat: + case TaikoSkinComponents.TaikoExplosionMiss: + case TaikoSkinComponents.TaikoExplosionOk: + return new ArgonHitExplosion(catchComponent.Component); } break; From b15d1bc333c9a379c56e0bd7466a4b7795b9fc77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 16:31:24 +0900 Subject: [PATCH 045/152] Add argon result display implementation --- .../Skinning/Argon/ArgonJudgementPiece.cs | 198 ++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 4 + 2 files changed, 202 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs new file mode 100644 index 0000000000..0ea0473023 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs @@ -0,0 +1,198 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement + { + protected readonly HitResult Result; + + protected SpriteText JudgementText { get; private set; } = null!; + + private RingExplosion? ringExplosion; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public ArgonJudgementPiece(HitResult result) + { + Result = result; + RelativePositionAxes = Axes.Both; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + JudgementText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = Result.GetDescription().ToUpperInvariant(), + Colour = colours.ForHitResult(Result), + Blending = BlendingParameters.Additive, + Spacing = new Vector2(10, 0), + RelativePositionAxes = Axes.Both, + Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular), + }, + }; + + if (Result.IsHit()) + { + AddInternal(ringExplosion = new RingExplosion(Result) + { + Colour = colours.ForHitResult(Result), + RelativePositionAxes = Axes.Y, + }); + } + } + + /// + /// Plays the default animation for this judgement piece. + /// + /// + /// The base implementation only handles fade (for all result types) and misses. + /// Individual rulesets are recommended to implement their appropriate hit animations. + /// + public virtual void PlayAnimation() + { + const double duration = 800; + + switch (Result) + { + default: + JudgementText.MoveToY(-0.2f) + .MoveToY(-0.8f, duration, Easing.OutQuint); + + JudgementText + .ScaleTo(Vector2.One) + .ScaleTo(new Vector2(1.8f), duration, Easing.OutQuint); + break; + + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + JudgementText.MoveTo(Vector2.Zero); + JudgementText.MoveToOffset(new Vector2(0, 100), duration, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, duration, Easing.InQuint); + break; + } + + this.FadeOutFromOne(duration, Easing.OutQuint); + + ringExplosion?.PlayAnimation(); + } + + public Drawable? GetAboveHitObjectsProxiedContent() => null; + + private class RingExplosion : CompositeDrawable + { + private readonly float travel = 58; + + public RingExplosion(HitResult result) + { + const float thickness = 4; + + const float small_size = 9; + const float large_size = 14; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Blending = BlendingParameters.Additive; + + int countSmall = 0; + int countLarge = 0; + + switch (result) + { + case HitResult.Meh: + countSmall = 3; + travel *= 0.3f; + break; + + case HitResult.Ok: + case HitResult.Good: + countSmall = 4; + travel *= 0.6f; + break; + + case HitResult.Great: + case HitResult.Perfect: + countSmall = 4; + countLarge = 4; + break; + } + + for (int i = 0; i < countSmall; i++) + AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) }); + + for (int i = 0; i < countLarge; i++) + AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) }); + } + + public void PlayAnimation() + { + foreach (var c in InternalChildren) + { + const float start_position_ratio = 0.6f; + + float direction = RNG.NextSingle(0, 360); + float distance = RNG.NextSingle(travel / 2, travel); + + c.MoveTo(new Vector2( + MathF.Cos(direction) * distance * start_position_ratio, + MathF.Sin(direction) * distance * start_position_ratio + )); + + c.MoveTo(new Vector2( + MathF.Cos(direction) * distance, + MathF.Sin(direction) * distance + ), 600, Easing.OutQuint); + } + + this.FadeOutFromOne(1000, Easing.OutQuint); + } + + public class RingPiece : CircularContainer + { + public RingPiece(float thickness = 9) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Masking = true; + BorderThickness = thickness; + BorderColour = Color4.White; + + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + }; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 3b75cfff11..a6e053f0df 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning.Argon @@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { switch (component) { + case GameplaySkinComponent resultComponent: + return new ArgonJudgementPiece(resultComponent.Component); + case TaikoSkinComponent catchComponent: // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. switch (catchComponent.Component) From d57ec4b227331f6264671cfb6776261aca76c4a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Nov 2022 17:29:27 +0900 Subject: [PATCH 046/152] Add argon input drum implementation --- .../Skinning/Argon/ArgonInputDrum.cs | 218 ++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 3 + 2 files changed, 221 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs new file mode 100644 index 0000000000..528e75aabb --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs @@ -0,0 +1,218 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Screens.Ranking; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonInputDrum : AspectContainer + { + private const float rim_size = 0.3f; + + public ArgonInputDrum() + { + RelativeSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + const float middle_split = 6; + + InternalChild = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.9f), + Children = new Drawable[] + { + new TaikoHalfDrum(false) + { + Name = "Left Half", + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre + }, + new TaikoHalfDrum(true) + { + Name = "Right Half", + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre + }, + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = OsuColour.Gray(38 / 255f), + Width = middle_split, + RelativeSizeAxes = Axes.Y, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = OsuColour.Gray(48 / 255f), + Width = middle_split, + Height = 1 - rim_size, + RelativeSizeAxes = Axes.Y, + }, + }, + } + } + }; + } + + /// + /// A half-drum. Contains one centre and one rim hit. + /// + private class TaikoHalfDrum : CompositeDrawable, IKeyBindingHandler + { + /// + /// The key to be used for the rim of the half-drum. + /// + public TaikoAction RimAction; + + /// + /// The key to be used for the centre of the half-drum. + /// + public TaikoAction CentreAction; + + private readonly Drawable rimHit; + private readonly Drawable centreHit; + + public TaikoHalfDrum(bool flipped) + { + Anchor anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight; + + Masking = true; + + Anchor = anchor; + Origin = anchor; + + RelativeSizeAxes = Axes.Both; + // Extend maskable region for glow. + Height = 2f; + + InternalChildren = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Children = new[] + { + new Circle + { + Anchor = anchor, + Colour = OsuColour.Gray(51 / 255f), + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both + }, + rimHit = new Circle + { + Anchor = anchor, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal( + new Color4(227, 248, 255, 255), + new Color4(198, 245, 255, 255) + ), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(126, 215, 253, 170), + Radius = 50, + }, + Alpha = 0, + }, + new Circle + { + Anchor = anchor, + Origin = Anchor.Centre, + Colour = OsuColour.Gray(64 / 255f), + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1 - rim_size) + }, + centreHit = new Circle + { + Anchor = anchor, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal( + new Color4(255, 227, 236, 255), + new Color4(255, 198, 211, 255) + ), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(255, 147, 199, 255), + Radius = 50, + }, + Size = new Vector2(1 - rim_size), + Alpha = 0, + } + }, + }, + }; + } + + public bool OnPressed(KeyBindingPressEvent e) + { + Drawable? target = null; + + if (e.Action == CentreAction) + target = centreHit; + else if (e.Action == RimAction) + target = rimHit; + + if (target != null) + { + const float alpha_amount = 0.5f; + + const float down_time = 40; + const float up_time = 750; + + target.Animate( + t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint) + ).Then( + t => t.FadeOut(up_time, Easing.OutQuint) + ); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index a6e053f0df..f0d14f657d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.PlayfieldBackgroundRight: return new ArgonPlayfieldBackgroundRight(); + case TaikoSkinComponents.InputDrum: + return new ArgonInputDrum(); + case TaikoSkinComponents.HitTarget: return new ArgonHitTarget(); From 64e627639762201399595de37d595fcc614db6fc Mon Sep 17 00:00:00 2001 From: maromalo <54760464+maromalo@users.noreply.github.com> Date: Tue, 8 Nov 2022 01:10:21 -0300 Subject: [PATCH 047/152] Fix score rounding issue --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index f619c89368..02512d857d 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -341,13 +341,13 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (long)((max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier); + return (long)Math.Round((max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier); case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score; - return (long)(Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); + return (long)Math.Round(Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); } } From 01803c3f13f0a46907401a5b1a3b670944d1ff09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Nov 2022 15:08:52 +0900 Subject: [PATCH 048/152] Adjust judgement text to be more visible --- .../Skinning/Argon/ArgonJudgementPiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs index 0ea0473023..baaf9e41e2 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonJudgementPiece.cs @@ -78,12 +78,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon switch (Result) { default: - JudgementText.MoveToY(-0.2f) - .MoveToY(-0.8f, duration, Easing.OutQuint); + JudgementText.MoveToY(-0.6f) + .MoveToY(-1.0f, duration, Easing.OutQuint); JudgementText .ScaleTo(Vector2.One) - .ScaleTo(new Vector2(1.8f), duration, Easing.OutQuint); + .ScaleTo(new Vector2(1.4f), duration, Easing.OutQuint); break; case HitResult.Miss: From 30890644a8c417119199de6f24fcd63cd366543b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Nov 2022 15:21:20 +0900 Subject: [PATCH 049/152] Flash piece when hit --- .../Skinning/Argon/ArgonCirclePiece.cs | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index 8ef7b71069..91f34e1f0e 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -33,17 +32,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon set { accentColour = value; + ring.Colour = AccentColour.MultiplyAlpha(0.5f); ring2.Colour = AccentColour; } } - /// - /// Whether Kiai mode effects are enabled for this circle piece. - /// - public bool KiaiMode { get; set; } + [Resolved] + private DrawableHitObject drawableHitObject { get; set; } = null!; - public Box FlashBox; + private readonly Drawable flash; private readonly RingPiece ring; private readonly RingPiece ring2; @@ -59,36 +57,43 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon new Circle { RelativeSizeAxes = Axes.Both, - Colour = new Color4(0, 22, 30, 190) + Colour = new Color4(0, 0, 0, 190) }, ring = new RingPiece(20 / 70f), ring2 = new RingPiece(5 / 70f), - new CircularContainer + flash = new Circle { Name = "Flash layer", Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] - { - FlashBox = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - AlwaysPresent = true - } - }, + Blending = BlendingParameters.Additive, + Alpha = 0, }, }); } - [Resolved] - private DrawableHitObject drawableHitObject { get; set; } = null!; + protected override void LoadComplete() + { + base.LoadComplete(); + + drawableHitObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableHitObject, drawableHitObject.State.Value); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + switch (state) + { + case ArmedState.Hit: + using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime)) + { + flash.FadeTo(0.9f).FadeOut(500, Easing.OutQuint); + } + + break; + } + } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { @@ -97,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon if (drawableHitObject.State.Value == ArmedState.Idle) { - FlashBox + flash .FadeTo(flash_opacity) .Then() .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); From d48c56818ff8ac446e67f7b0d12eb2088564c6c0 Mon Sep 17 00:00:00 2001 From: Piggey Date: Tue, 8 Nov 2022 20:02:21 +0100 Subject: [PATCH 050/152] change `positionText` to display "#?" if tracked score is being 51st on the leaderboard --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 4 ++++ osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 48908fb9a0..10049c7718 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -174,6 +174,10 @@ namespace osu.Game.Screens.Play.HUD orderedByScore[i].ScorePosition = i + 1; } + // change displayed potision to '#?' when there are 50 already submitted scores and tracked score is last + if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count == 51) + TrackedScore.ScorePosition = null; + sorting.Validate(); } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 2eec8253b3..dc2c0620c8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -72,10 +72,8 @@ namespace osu.Game.Screens.Play.HUD scorePosition = value; - if (scorePosition.HasValue) - positionText.Text = $"#{scorePosition.Value.FormatRank()}"; + positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "#?"; - positionText.FadeTo(scorePosition.HasValue ? 1 : 0); updateState(); } } From 9da57c66a2b29a0674c10659ba500c5758d40513 Mon Sep 17 00:00:00 2001 From: Piggey Date: Tue, 8 Nov 2022 20:03:09 +0100 Subject: [PATCH 051/152] add visual tests --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 171ae829a9..a385060d62 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add many scores in one go", () => { - for (int i = 0; i < 32; i++) + for (int i = 0; i < 49; i++) createRandomScore(new APIUser { Username = $"Player {i + 1}" }); // Add player at end to force an animation down the whole list. @@ -61,6 +61,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); + AddUntilStep("ensure player is #50", () => leaderboard.CheckPositionByUsername("You", 50)); + + AddStep("add one more player", () => createRandomScore(new APIUser { Username = "Player 50" })); + + AddUntilStep("ensure player is #?", () => leaderboard.CheckPositionByUsername("You", null)); + AddStep("change score to middle", () => playerScore.Value = 1000000); AddWaitStep("wait for movement", 5); AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); From ab53fb17d390fc8cc8667d720b5e8f7c8a5b9142 Mon Sep 17 00:00:00 2001 From: Piggey Date: Wed, 9 Nov 2022 19:53:36 +0100 Subject: [PATCH 052/152] change displayed text from `#?` to `-` --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index dc2c0620c8..062b31e05a 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD scorePosition = value; - positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "#?"; + positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-"; updateState(); } From 0011f4e7be99561ac22a3eb8d07cd32adce71ed3 Mon Sep 17 00:00:00 2001 From: Piggey Date: Wed, 9 Nov 2022 19:59:04 +0100 Subject: [PATCH 053/152] fix comment lol --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 10049c7718..c8631880ac 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Play.HUD orderedByScore[i].ScorePosition = i + 1; } - // change displayed potision to '#?' when there are 50 already submitted scores and tracked score is last + // change displayed potision to '-' when there are 50 already submitted scores and tracked score is last if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count == 51) TrackedScore.ScorePosition = null; From 83a3f1b82e85e981c2117448686bd1c6ad471b67 Mon Sep 17 00:00:00 2001 From: maromalo <54760464+maromalo@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:15:20 -0300 Subject: [PATCH 054/152] Add computeRawScore() --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 02512d857d..2b6e40b94e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -339,16 +339,21 @@ namespace osu.Game.Rulesets.Scoring { default: case ScoringMode.Standardised: - double accuracyScore = accuracyPortion * accuracyRatio; - double comboScore = comboPortion * comboRatio; - return (long)Math.Round((max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier); + return (long)Math.Round(computeRawScore()); case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. - double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score; + double scaledStandardised = computeRawScore() / max_score; return (long)Math.Round(Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); } + + double computeRawScore() + { + double accuracyScore = accuracyPortion * accuracyRatio; + double comboScore = comboPortion * comboRatio; + return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + } } private ScoreRank rankFrom(double acc) From 6ac19615fa2669c53a86d6aee51c19f90e7158dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 11 Nov 2022 14:55:41 +0900 Subject: [PATCH 055/152] Fix test failure --- osu.Game.Tests/Chat/TestSceneChannelManager.cs | 12 +++++++++++- osu.Game/Online/Chat/Channel.cs | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs index e7eb06c795..32fc2604ba 100644 --- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs +++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs @@ -55,6 +55,14 @@ namespace osu.Game.Tests.Chat case MarkChannelAsReadRequest markRead: handleMarkChannelAsReadRequest(markRead); return true; + + case GetUpdatesRequest updatesRequest: + updatesRequest.TriggerSuccess(new GetUpdatesResponse + { + Messages = sentMessages.ToList(), + Presence = new List() + }); + return true; } return false; @@ -95,6 +103,7 @@ namespace osu.Game.Tests.Chat }); AddStep("post message", () => channelManager.PostMessage("Something interesting")); + AddUntilStep("message postesd", () => !channel.Messages.Any(m => m is LocalMessage)); AddStep("post /help command", () => channelManager.PostCommand("help", channel)); AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel)); @@ -115,7 +124,8 @@ namespace osu.Game.Tests.Chat Content = request.Message.Content, Links = request.Message.Links, Timestamp = request.Message.Timestamp, - Sender = request.Message.Sender + Sender = request.Message.Sender, + Uuid = request.Message.Uuid }; sentMessages.Add(message); diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 9bfaecc46b..ada9e22027 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -179,6 +179,10 @@ namespace osu.Game.Online.Chat throw new InvalidOperationException("Attempted to add the same message again"); Messages.Add(final); + + if (final.Id > LastMessageId) + LastMessageId = final.Id; + PendingMessageResolved?.Invoke(echo, final); } From 7ef11cab8b76ea75ec5f1671fa0dbc6f73685361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Nov 2022 02:10:10 +0900 Subject: [PATCH 056/152] Adjust taiko argon transformer to new naming --- .../Skinning/Argon/TaikoArgonSkinTransformer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index f0d14f657d..379c675a0f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -14,14 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { } - public override Drawable? GetDrawableComponent(ISkinComponent component) + public override Drawable? GetDrawableComponent(ISkinComponentLookup component) { switch (component) { - case GameplaySkinComponent resultComponent: + case GameplaySkinComponentLookup resultComponent: return new ArgonJudgementPiece(resultComponent.Component); - case TaikoSkinComponent catchComponent: + case TaikoSkinComponentLookup catchComponent: // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. switch (catchComponent.Component) { From 6a4c97b4f119bb91e73eb48c832691665da4fffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Nov 2022 02:20:19 +0900 Subject: [PATCH 057/152] Fix code inspection --- osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index 91f34e1f0e..22f96da61e 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon EarlyActivationMilliseconds = pre_beat_transition_time; - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { new Circle { From 8b8147c3212d4475992f48df3ab3498b17c9925c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Nov 2022 03:05:03 +0900 Subject: [PATCH 058/152] Rename `{catch -> taiko}Component` --- .../Skinning/Argon/TaikoArgonSkinTransformer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 379c675a0f..6d1087793d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -21,9 +21,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case GameplaySkinComponentLookup resultComponent: return new ArgonJudgementPiece(resultComponent.Component); - case TaikoSkinComponentLookup catchComponent: + case TaikoSkinComponentLookup taikoComponent: // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries. - switch (catchComponent.Component) + switch (taikoComponent.Component) { case TaikoSkinComponents.CentreHit: return new ArgonCentreCirclePiece(); @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionOk: - return new ArgonHitExplosion(catchComponent.Component); + return new ArgonHitExplosion(taikoComponent.Component); } break; From b0314c67aa6f21aaae6e9b32be23ceac8b86cafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Nov 2022 14:16:46 +0900 Subject: [PATCH 059/152] Fix failing gameplay bindings test --- .../Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 5bd879de14..c3d7bde68f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => new PlaySongSelect()); AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault); + AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); @@ -92,6 +93,8 @@ namespace osu.Game.Tests.Visual.Navigation .AsEnumerable() .First(k => k.RulesetName == "osu" && k.ActionInt == 0); + private Screens.Select.SongSelect songSelect => Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect; + private Player player => Game.ScreenStack.CurrentScreen as Player; private KeyCounter keyCounter => player.ChildrenOfType().First(); From fa8e38d9d61cc867f9ff37e991693df6b936bb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Sat, 12 Nov 2022 17:05:29 +0900 Subject: [PATCH 060/152] Target rounded fragment shader Compatibility with osu-framework change https://github.com/ppy/osu-framework/pull/5512 --- osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs | 2 -- osu.Game/Graphics/Backgrounds/Triangles.cs | 2 +- osu.Game/Graphics/Sprites/LogoAnimation.cs | 1 - osu.Game/Screens/Loader.cs | 4 +--- osu.Game/Screens/Menu/LogoVisualisation.cs | 2 +- 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs index 46c8e7c02a..3bc17c0849 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Skinning private const float max_rotation = 0.25f; public IShader? TextureShader { get; private set; } - public IShader? RoundedTextureShader { get; private set; } protected Texture? Texture { get; set; } @@ -69,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ShaderManager shaders) { - RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); } diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 1166a86814..986f5d3ac5 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Backgrounds private void load(IRenderer renderer, ShaderManager shaders) { texture = renderer.WhitePixel; - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); } protected override void LoadComplete() diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs index 7debdc7a37..5233a3d67f 100644 --- a/osu.Game/Graphics/Sprites/LogoAnimation.cs +++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs @@ -17,7 +17,6 @@ namespace osu.Game.Graphics.Sprites private void load(ShaderManager shaders) { TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); - RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now } private float animationProgress; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index afba00274c..ac22fdce71 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -125,13 +125,11 @@ namespace osu.Game.Screens [BackgroundDependencyLoader] private void load(ShaderManager manager) { - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); } diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index c7bda4d8f8..4a20d7cb2b 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Menu private void load(IRenderer renderer, ShaderManager shaders) { texture = renderer.WhitePixel; - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); } private readonly float[] temporalAmplitudes = new float[ChannelAmplitudes.AMPLITUDES_SIZE]; From aef25df80844444e517f9bdd9be2bab17dc5a979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Nov 2022 17:06:08 +0900 Subject: [PATCH 061/152] Add helper scripts for using local resources --- UseLocalResources.ps1 | 12 ++++++++++++ UseLocalResources.sh | 11 +++++++++++ 2 files changed, 23 insertions(+) create mode 100644 UseLocalResources.ps1 create mode 100755 UseLocalResources.sh diff --git a/UseLocalResources.ps1 b/UseLocalResources.ps1 new file mode 100644 index 0000000000..f9d9df01bb --- /dev/null +++ b/UseLocalResources.ps1 @@ -0,0 +1,12 @@ +$CSPROJ="osu.Game/osu.Game.csproj" +$SLN="osu.sln" + +dotnet remove $CSPROJ package ppy.osu.Game.Resources; +dotnet sln $SLN add ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj +dotnet add $CSPROJ reference ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj + +$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json +$TMP=New-TemporaryFile +$SLNF.solution.projects += ("../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj") +ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8 +Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force diff --git a/UseLocalResources.sh b/UseLocalResources.sh new file mode 100755 index 0000000000..6d9d2b6016 --- /dev/null +++ b/UseLocalResources.sh @@ -0,0 +1,11 @@ +CSPROJ="osu.Game/osu.Game.csproj" +SLN="osu.sln" + +dotnet remove $CSPROJ package ppy.osu.Game.Resources; +dotnet sln $SLN add ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj +dotnet add $CSPROJ reference ../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj + +SLNF="osu.Desktop.slnf" +TMP=$(mktemp) +jq '.solution.projects += ["../osu-resources/osu.Game.Resources/osu.Game.Resources.csproj"]' $SLNF > $TMP +mv -f $TMP $SLNF From 9ef43ebd837e44f8f8cb3c3c5629ec44c7adcb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Sat, 12 Nov 2022 21:24:12 +0900 Subject: [PATCH 062/152] Fix compilation after framework-side PR updates --- osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs | 6 ++---- osu.Game/Graphics/Sprites/LogoAnimation.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs index 3bc17c0849..c19ed3fb35 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -245,18 +245,16 @@ namespace osu.Game.Rulesets.Osu.Skinning texture ??= renderer.WhitePixel; RectangleF textureRect = texture.GetTextureRect(); - var shader = GetAppropriateShader(renderer); - renderer.SetBlend(BlendingParameters.Additive); renderer.PushLocalMatrix(DrawInfo.Matrix); - shader.Bind(); + TextureShader.Bind(); texture.Bind(); for (int i = 0; i < points.Count; i++) drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex); - shader.Unbind(); + TextureShader.Unbind(); renderer.PopLocalMatrix(); } diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs index 5233a3d67f..097de4dfcb 100644 --- a/osu.Game/Graphics/Sprites/LogoAnimation.cs +++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs @@ -57,7 +57,7 @@ namespace osu.Game.Graphics.Sprites protected override void Blit(IRenderer renderer) { - GetAppropriateShader(renderer).GetUniform("progress").UpdateValue(ref progress); + TextureShader.GetUniform("progress").UpdateValue(ref progress); base.Blit(renderer); } From ace4099079889f8b220d8e8514167870dac0cad8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Nov 2022 21:41:10 +0900 Subject: [PATCH 063/152] Update ack code after incorrect merge --- osu.Game/Online/Chat/ChannelManager.cs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 0aa86fb9d1..bb388b7461 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -116,20 +116,7 @@ namespace osu.Game.Online.Chat if (apiState.Value != APIState.Online) return; - scheduledAck?.Cancel(); - - var req = new ChatAckRequest(); - req.Success += _ => scheduleNextRequest(); - req.Failure += _ => scheduleNextRequest(); - api.Queue(req); - - // Todo: Handle silences. - - void scheduleNextRequest() - { - scheduledAck?.Cancel(); - scheduledAck = Scheduler.AddDelayed(performChatAckRequest, 60000); - } + SendAck(); } /// @@ -416,6 +403,7 @@ namespace osu.Game.Online.Chat SinceSilenceId = lastSilenceId }; + req.Failure += _ => scheduleNextRequest(); req.Success += ack => { foreach (var silence in ack.Silences) @@ -424,9 +412,17 @@ namespace osu.Game.Online.Chat channel.RemoveMessagesFromUser(silence.UserId); lastSilenceId = Math.Max(lastSilenceId ?? 0, silence.Id); } + + scheduleNextRequest(); }; api.Queue(req); + + void scheduleNextRequest() + { + scheduledAck?.Cancel(); + scheduledAck = Scheduler.AddDelayed(performChatAckRequest, 60000); + } } /// From 6a3665a6fde45b4f66e977cd79aa5a6670ebd3d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Nov 2022 22:09:27 +0900 Subject: [PATCH 064/152] Remove excess logging on `WebSocketNotificaitonsClient` --- .../Notifications/WebSocket/WebSocketNotificationsClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs index 86836099d8..d8d78297e3 100644 --- a/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs +++ b/osu.Game/Online/Notifications/WebSocket/WebSocketNotificationsClient.cs @@ -72,7 +72,6 @@ namespace osu.Game.Online.Notifications.WebSocket break; } - Logger.Log($"{GetType().ReadableName()} handling event: {message.Event}"); await onMessageReceivedAsync(message); } From bfb939cbd0ae1b1528d23534457a26178cf340d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Nov 2022 22:24:27 +0900 Subject: [PATCH 065/152] Expand on why call needs to be made every 10 minutes --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index bb388b7461..ebfe44bd66 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -393,7 +393,7 @@ namespace osu.Game.Online.Chat /// /// Sends an acknowledgement request to the API. /// This marks the user as online to receive messages from public channels, while also returning a list of silenced users. - /// It needs to be called at least once every 10 minutes. + /// It needs to be called at least once every 10 minutes to remain visibly marked as online. /// public void SendAck() { From 143c94612872c8ea6c804d51b956f3f6a59e1ce5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Nov 2022 23:02:37 +0900 Subject: [PATCH 066/152] Simplify ack re-perform flow --- osu.Game/Online/Chat/ChannelManager.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index ebfe44bd66..8f1d2db36c 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -108,15 +108,7 @@ namespace osu.Game.Online.Chat connector.Start(); apiState.BindTo(api.State); - apiState.BindValueChanged(_ => performChatAckRequest(), true); - } - - private void performChatAckRequest() - { - if (apiState.Value != APIState.Online) - return; - - SendAck(); + apiState.BindValueChanged(_ => SendAck(), true); } /// @@ -397,6 +389,9 @@ namespace osu.Game.Online.Chat /// public void SendAck() { + if (apiState.Value != APIState.Online) + return; + var req = new ChatAckRequest { SinceMessageId = lastMessageId, @@ -421,7 +416,7 @@ namespace osu.Game.Online.Chat void scheduleNextRequest() { scheduledAck?.Cancel(); - scheduledAck = Scheduler.AddDelayed(performChatAckRequest, 60000); + scheduledAck = Scheduler.AddDelayed(SendAck, 60000); } } From 22d8a1160e28bae031040fa5376dcfaa0caf219b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Nov 2022 23:32:05 +0900 Subject: [PATCH 067/152] Fix last silence ID being updated too often, causing most silences to be missed --- osu.Game/Online/API/Requests/ChatAckRequest.cs | 5 +++-- osu.Game/Online/Chat/ChannelManager.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/API/Requests/ChatAckRequest.cs b/osu.Game/Online/API/Requests/ChatAckRequest.cs index 78f51e21c0..01b0c142dc 100644 --- a/osu.Game/Online/API/Requests/ChatAckRequest.cs +++ b/osu.Game/Online/API/Requests/ChatAckRequest.cs @@ -9,14 +9,15 @@ namespace osu.Game.Online.API.Requests { public class ChatAckRequest : APIRequest { - public long SinceMessageId; + public long? SinceMessageId; public uint? SinceSilenceId; protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; - req.AddParameter(@"since", SinceMessageId.ToString()); + if (SinceMessageId != null) + req.AddParameter(@"since", SinceMessageId.ToString()); if (SinceSilenceId != null) req.AddParameter(@"history_since", SinceSilenceId.Value.ToString()); return req; diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 8f1d2db36c..25a53360f0 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -74,7 +74,7 @@ namespace osu.Game.Online.Chat private bool channelsInitialised; private ScheduledDelegate scheduledAck; - private long lastMessageId; + private long? lastSilenceMessageId; private uint? lastSilenceId; public ChannelManager(IAPIProvider api) @@ -332,7 +332,7 @@ namespace osu.Game.Online.Chat foreach (var group in messages.GroupBy(m => m.ChannelId)) channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); - lastMessageId = messages.LastOrDefault()?.Id ?? lastMessageId; + lastSilenceMessageId ??= messages.LastOrDefault()?.Id; } private void initializeChannels() @@ -394,7 +394,7 @@ namespace osu.Game.Online.Chat var req = new ChatAckRequest { - SinceMessageId = lastMessageId, + SinceMessageId = lastSilenceMessageId, SinceSilenceId = lastSilenceId }; From f343ba611139c98a0424f95c8a07dd0ee4d03b7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Nov 2022 23:35:21 +0900 Subject: [PATCH 068/152] Add xmldoc for chat ack request --- osu.Game/Online/API/Requests/ChatAckRequest.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Online/API/Requests/ChatAckRequest.cs b/osu.Game/Online/API/Requests/ChatAckRequest.cs index 01b0c142dc..306b5acc1d 100644 --- a/osu.Game/Online/API/Requests/ChatAckRequest.cs +++ b/osu.Game/Online/API/Requests/ChatAckRequest.cs @@ -7,6 +7,17 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { + /// + /// A request which should be sent occasionally while interested in chat and online state. + /// + /// This will: + /// - Mark the user as "online" (for 10 minutes since the last invocation). + /// - Return any silences since the last invocation (if either or is not null). + /// + /// For silence handling, a should be provided as soon as a message is received by the client. + /// From that point forward, should be preferred after the first + /// arrives in a response from the ack request. Specifying both parameters will prioritise the latter. + /// public class ChatAckRequest : APIRequest { public long? SinceMessageId; From eae853072298e1251f8a0cfdeecb2cead746fa23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Nov 2022 12:46:20 +0900 Subject: [PATCH 069/152] Fix `SkinnableSprite` lookups broken in lazer-first skins Regressed with removal of local `GetTexture` calls in https://github.com/ppy/osu/commit/e19ba65f9186afab21aec536a636d7d152636dde --- osu.Game/Skinning/ArgonSkin.cs | 13 +++---------- osu.Game/Skinning/LegacySkin.cs | 3 --- osu.Game/Skinning/Skin.cs | 4 ++++ osu.Game/Skinning/TrianglesSkin.cs | 13 +++---------- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index a2eb07eba3..6a0c4a23e5 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -82,21 +82,14 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { + // Temporary until default skin has a valid hit lighting. + if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); + if (base.GetDrawableComponent(lookup) is Drawable c) return c; switch (lookup) { - case SkinnableSprite.SpriteComponentLookup spriteLookup: - switch (spriteLookup.LookupName) - { - // Temporary until default skin has a valid hit lighting. - case @"lighting": - return Drawable.Empty(); - } - - break; - case GlobalSkinComponentLookup globalLookup: switch (globalLookup.Lookup) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 98618e3dcd..ea223d172d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -396,9 +396,6 @@ namespace osu.Game.Skinning } return null; - - case SkinnableSprite.SpriteComponentLookup sprite: - return this.GetAnimation(sprite.LookupName, false, false); } return null; diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e222b9017c..25d1dc903c 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -158,6 +158,10 @@ namespace osu.Game.Skinning { switch (lookup) { + // This fallback is important for user skins which use SkinnableSprites. + case SkinnableSprite.SpriteComponentLookup sprite: + return this.GetAnimation(sprite.LookupName, false, false); + case GlobalSkinComponentLookup target: if (!DrawableComponentInfo.TryGetValue(target.Lookup, out var skinnableInfo)) return null; diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 2075cfb6f2..62ef94691b 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -60,21 +60,14 @@ namespace osu.Game.Skinning public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) { + // Temporary until default skin has a valid hit lighting. + if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); + if (base.GetDrawableComponent(lookup) is Drawable c) return c; switch (lookup) { - case SkinnableSprite.SpriteComponentLookup spriteLookup: - switch (spriteLookup.LookupName) - { - // Temporary until default skin has a valid hit lighting. - case @"lighting": - return Drawable.Empty(); - } - - break; - case GlobalSkinComponentLookup target: switch (target.Lookup) { From 887b6832c9435fce99ad7d10095a26f52fcec572 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Nov 2022 16:15:30 +0900 Subject: [PATCH 070/152] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8237a570ff..085b71d131 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 09b1bb7162..8f9fe0f9f7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 4264d9220e..e8ccc9cb3a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + @@ -82,7 +82,7 @@ - + From 236cc0bdaf10861546efe3b328ac01d63cd546c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Nov 2022 16:21:28 +0900 Subject: [PATCH 071/152] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 085b71d131..5f3fb858ee 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8f9fe0f9f7..858cac1dac 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index e8ccc9cb3a..25217b872b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + From 4578a968131a0c0ca68732fa1c0420ddd0c9d2fb Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 13 Nov 2022 08:18:44 -0800 Subject: [PATCH 072/152] Fix beatmap card expanded content not blocking clicks from behind --- .../Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs index a80a7998a5..c0ed6ac1a9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/ExpandedContentScrollContainer.cs @@ -59,6 +59,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards return base.OnScroll(e); } + protected override bool OnClick(ClickEvent e) => true; + private class ExpandedContentScrollbar : OsuScrollbar { public ExpandedContentScrollbar(Direction scrollDir) From 56fd1f95b150b51ca839e08a4fbf31d8b97d1c9b Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 13 Nov 2022 18:47:28 +0100 Subject: [PATCH 073/152] Fix `FallbackSampleStore.GetAsync` fallback logic --- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index ad52b4affc..c50f63c3b2 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -115,7 +115,11 @@ namespace osu.Game.Rulesets.UI public Sample Get(string name) => primary.Get(name) ?? fallback.Get(name); - public Task GetAsync(string name, CancellationToken cancellationToken = default) => primary.GetAsync(name, cancellationToken) ?? fallback.GetAsync(name, cancellationToken); + public async Task GetAsync(string name, CancellationToken cancellationToken = default) + { + return await primary.GetAsync(name, cancellationToken).ConfigureAwait(false) + ?? await fallback.GetAsync(name, cancellationToken).ConfigureAwait(false); + } public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name); From a8c95c39ad0dd19791f025497f27d61fbf77adf6 Mon Sep 17 00:00:00 2001 From: Dario Headley Date: Mon, 14 Nov 2022 16:18:36 +0100 Subject: [PATCH 074/152] Exclude sliderticks from the " freeze " --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index bea5d4f5d9..0a1aab9ef1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods { switch (nested) { + //Freezing the SliderTicks doesnt play well with snaking sliders + case SliderTick: //SliderRepeat wont layer correctly if preempt is changed. case SliderRepeat: break; From 576f462f59e9cd6110582a611a01a25ebc71c057 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Nov 2022 00:53:38 +0900 Subject: [PATCH 075/152] Add pooling support to `BarHitErrorMeter` --- .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index e7b2ce1672..5fca8e2c51 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; @@ -23,6 +24,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { + [Cached] public class BarHitErrorMeter : HitErrorMeter { private const int judgement_line_width = 14; @@ -44,6 +46,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [SettingSource("Label style", "How to show early/late extremities")] public Bindable LabelStyle { get; } = new Bindable(LabelStyles.Icons); + private readonly DrawablePool judgementLinePool = new DrawablePool(50, 100); + private SpriteIcon arrow; private UprightAspectMaintainingContainer labelEarly; private UprightAspectMaintainingContainer labelLate; @@ -88,6 +92,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters Margin = new MarginPadding(2), Children = new Drawable[] { + judgementLinePool, colourBars = new Container { Name = "colour axis", @@ -403,11 +408,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters } } - judgementsContainer.Add(new JudgementLine + judgementLinePool.Get(drawableJudgement => { - JudgementLineThickness = { BindTarget = JudgementLineThickness }, - Y = getRelativeJudgementPosition(judgement.TimeOffset), - Colour = GetColourForHitResult(judgement.Type), + drawableJudgement.Y = getRelativeJudgementPosition(judgement.TimeOffset); + drawableJudgement.Colour = GetColourForHitResult(judgement.Type); + + judgementsContainer.Add(drawableJudgement); }); arrow.MoveToY( @@ -417,7 +423,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1); - internal class JudgementLine : CompositeDrawable + internal class JudgementLine : PoolableDrawable { public readonly BindableNumber JudgementLineThickness = new BindableFloat(); @@ -437,18 +443,27 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters }; } + [Resolved] + private BarHitErrorMeter barHitErrorMeter { get; set; } + protected override void LoadComplete() { + base.LoadComplete(); + + JudgementLineThickness.BindTo(barHitErrorMeter.JudgementLineThickness); + JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + const int judgement_fade_in_duration = 100; const int judgement_fade_out_duration = 5000; - base.LoadComplete(); - Alpha = 0; Width = 0; - JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true); - this .FadeTo(0.6f, judgement_fade_in_duration, Easing.OutQuint) .ResizeWidthTo(1, judgement_fade_in_duration, Easing.OutQuint) From a86b50d62afd9cd279622f39d31505423ecee6fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Nov 2022 01:07:53 +0900 Subject: [PATCH 076/152] Apply nullability to `BarHitErrorMeter` --- .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 5fca8e2c51..23e9c86adc 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Allocation; @@ -27,8 +25,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Cached] public class BarHitErrorMeter : HitErrorMeter { - private const int judgement_line_width = 14; - [SettingSource("Judgement line thickness", "How thick the individual lines should be.")] public BindableNumber JudgementLineThickness { get; } = new BindableNumber(4) { @@ -46,30 +42,33 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [SettingSource("Label style", "How to show early/late extremities")] public Bindable LabelStyle { get; } = new Bindable(LabelStyles.Icons); - private readonly DrawablePool judgementLinePool = new DrawablePool(50, 100); + private const int judgement_line_width = 14; - private SpriteIcon arrow; - private UprightAspectMaintainingContainer labelEarly; - private UprightAspectMaintainingContainer labelLate; + private const int max_concurrent_judgements = 50; - private Container colourBarsEarly; - private Container colourBarsLate; - - private Container judgementsContainer; + private const int centre_marker_size = 8; private double maxHitWindow; private double floatingAverage; - private Container colourBars; - private Container arrowContainer; - private (HitResult result, double length)[] hitWindows; + private readonly DrawablePool judgementLinePool = new DrawablePool(50, 100); - private const int max_concurrent_judgements = 50; + private SpriteIcon arrow = null!; + private UprightAspectMaintainingContainer labelEarly = null!; + private UprightAspectMaintainingContainer labelLate = null!; - private Drawable[] centreMarkerDrawables; + private Container colourBarsEarly = null!; + private Container colourBarsLate = null!; - private const int centre_marker_size = 8; + private Container judgementsContainer = null!; + + private Container colourBars = null!; + private Container arrowContainer = null!; + + private (HitResult result, double length)[] hitWindows = null!; + + private Drawable[]? centreMarkerDrawables; public BarHitErrorMeter() { @@ -427,6 +426,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { public readonly BindableNumber JudgementLineThickness = new BindableFloat(); + [Resolved] + private BarHitErrorMeter barHitErrorMeter { get; set; } = null!; + public JudgementLine() { RelativeSizeAxes = Axes.X; @@ -443,9 +445,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters }; } - [Resolved] - private BarHitErrorMeter barHitErrorMeter { get; set; } - protected override void LoadComplete() { base.LoadComplete(); From aef6ee23eb871d7c7ae1550b4d451527a623d630 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Nov 2022 01:34:22 +0900 Subject: [PATCH 077/152] Apply pooling support to `ColourHitErrorMeter` --- .../HUD/HitErrorMeters/ColourHitErrorMeter.cs | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index dadec7c06b..6bf3ad0b28 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; @@ -15,6 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { + [Cached] public class ColourHitErrorMeter : HitErrorMeter { private const int animation_duration = 200; @@ -82,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { base.LoadComplete(); - JudgementCount.BindValueChanged(count => + JudgementCount.BindValueChanged(_ => { removeExtraJudgements(); updateMetrics(); @@ -91,11 +94,14 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters JudgementSpacing.BindValueChanged(_ => updateMetrics(), true); } + private readonly DrawablePool judgementLinePool = new DrawablePool(50); + public void Push(Color4 colour) { - Add(new HitErrorShape(colour, drawable_judgement_size) + judgementLinePool.Get(shape => { - Shape = { BindTarget = JudgementShape }, + shape.Colour = colour; + Add(shape); }); removeExtraJudgements(); @@ -116,32 +122,32 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters } } - public class HitErrorShape : Container + public class HitErrorShape : PoolableDrawable { public bool IsRemoved { get; private set; } public readonly Bindable Shape = new Bindable(); - private readonly Color4 colour; + [Resolved] + private ColourHitErrorMeter hitErrorMeter { get; set; } = null!; private Container content = null!; - public HitErrorShape(Color4 colour, int size) + public HitErrorShape() { - this.colour = colour; - Size = new Vector2(size); + Size = new Vector2(drawable_judgement_size); } protected override void LoadComplete() { base.LoadComplete(); - Child = content = new Container + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both, - Colour = colour }; + Shape.BindTo(hitErrorMeter.JudgementShape); Shape.BindValueChanged(shape => { switch (shape.NewValue) @@ -155,17 +161,27 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters break; } }, true); + } - content.FadeInFromZero(animation_duration, Easing.OutQuint); - content.MoveToY(-DrawSize.Y); - content.MoveToY(0, animation_duration, Easing.OutQuint); + protected override void PrepareForUse() + { + base.PrepareForUse(); + + IsRemoved = false; + + this.FadeIn(); + + content.FadeInFromZero(animation_duration, Easing.OutQuint) + .MoveToY(-DrawSize.Y) + .MoveToY(0, animation_duration, Easing.OutQuint); } public void Remove() { IsRemoved = true; - this.FadeOut(animation_duration, Easing.OutQuint).Expire(); + this.FadeOut(animation_duration, Easing.OutQuint) + .Expire(); } } From 59a8603728e280e8cd75e620a89ac7ba4d478bf6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Nov 2022 11:33:19 +0900 Subject: [PATCH 078/152] Ensure flowing animation starts correctly from zero --- .../Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index 6bf3ad0b28..b82afe18a6 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -169,7 +169,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters IsRemoved = false; - this.FadeIn(); + this.FadeIn() + // On pool re-use, start flow animation from (0,0). + .MoveTo(Vector2.Zero); content.FadeInFromZero(animation_duration, Easing.OutQuint) .MoveToY(-DrawSize.Y) From 308ed1abd1353631d98f1c868fd010d0c989b807 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Nov 2022 11:55:33 +0900 Subject: [PATCH 079/152] Fix number of judgements shown potentially exceeding upper limit --- .../Visual/Gameplay/TestSceneHitErrorMeter.cs | 5 +---- .../HUD/HitErrorMeters/ColourHitErrorMeter.cs | 17 ++++++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 7c668adba5..b90b9b437d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -163,10 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for bars to disappear", () => !this.ChildrenOfType().Any()); AddUntilStep("ensure max circles not exceeded", () => - { - return this.ChildrenOfType() - .All(m => m.ChildrenOfType().Count() <= max_displayed_judgements); - }); + this.ChildrenOfType().First().ChildrenOfType().Count(), () => Is.LessThanOrEqualTo(max_displayed_judgements)); AddStep("show displays", () => { diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index b82afe18a6..86ba85168f 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -102,9 +102,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { shape.Colour = colour; Add(shape); - }); - removeExtraJudgements(); + removeExtraJudgements(); + }); } private void removeExtraJudgements() @@ -167,17 +167,20 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { base.PrepareForUse(); - IsRemoved = false; - - this.FadeIn() + this.FadeInFromZero(animation_duration, Easing.OutQuint) // On pool re-use, start flow animation from (0,0). .MoveTo(Vector2.Zero); - content.FadeInFromZero(animation_duration, Easing.OutQuint) - .MoveToY(-DrawSize.Y) + content.MoveToY(-DrawSize.Y) .MoveToY(0, animation_duration, Easing.OutQuint); } + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + IsRemoved = false; + } + public void Remove() { IsRemoved = true; From 489dca79a115060fe76cfa08d5a94b0886eec923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Nov 2022 12:05:38 +0900 Subject: [PATCH 080/152] Remove upper limit for pooling --- osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 23e9c86adc..c9f1571dfe 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private double floatingAverage; - private readonly DrawablePool judgementLinePool = new DrawablePool(50, 100); + private readonly DrawablePool judgementLinePool = new DrawablePool(50); private SpriteIcon arrow = null!; private UprightAspectMaintainingContainer labelEarly = null!; From 333165e0528f2f48d427d8cf215efcd01bfb886e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 15 Nov 2022 10:54:37 +0300 Subject: [PATCH 081/152] Add test scene for Triangles --- .../TestSceneTrianglesBackground.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs new file mode 100644 index 0000000000..81a3249efb --- /dev/null +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.Backgrounds; +using osu.Framework.Graphics; +using osuTK.Graphics; +using osu.Framework.Graphics.Shapes; + +namespace osu.Game.Tests.Visual.Background +{ + public class TestSceneTrianglesBackground : OsuTestScene + { + private readonly Triangles triangles; + + public TestSceneTrianglesBackground() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + triangles = new Triangles + { + RelativeSizeAxes = Axes.Both, + ColourLight = Color4.White, + ColourDark = Color4.Gray + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); + } + } +} From ebff8443340c66acf7fd41deb1feaf7ef1e14080 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 15 Nov 2022 11:06:28 +0300 Subject: [PATCH 082/152] Reset triangles on scale change --- osu.Game/Graphics/Backgrounds/Triangles.cs | 34 ++++++++-------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 986f5d3ac5..b5e77ec638 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Lists; +using osu.Framework.Bindables; namespace osu.Game.Graphics.Backgrounds { @@ -69,7 +70,13 @@ namespace osu.Game.Graphics.Backgrounds /// protected virtual float SpawnRatio => 1; - private float triangleScale = 1; + private readonly BindableFloat triangleScale = new BindableFloat(1f); + + public float TriangleScale + { + get => triangleScale.Value; + set => triangleScale.Value = value; + } /// /// Whether we should drop-off alpha values of triangles more quickly to improve @@ -109,24 +116,7 @@ namespace osu.Game.Graphics.Backgrounds protected override void LoadComplete() { base.LoadComplete(); - addTriangles(true); - } - - public float TriangleScale - { - get => triangleScale; - set - { - float change = value / triangleScale; - triangleScale = value; - - for (int i = 0; i < parts.Count; i++) - { - TriangleParticle newParticle = parts[i]; - newParticle.Scale *= change; - parts[i] = newParticle; - } - } + triangleScale.BindValueChanged(_ => Reset(), true); } protected override void Update() @@ -147,7 +137,7 @@ namespace osu.Game.Graphics.Backgrounds // Since position is relative, the velocity needs to scale inversely with DrawHeight. // Since we will later multiply by the scale of individual triangles we normalize by // dividing by triangleScale. - float movedDistance = -elapsedSeconds * Velocity * base_velocity / (DrawHeight * triangleScale); + float movedDistance = -elapsedSeconds * Velocity * base_velocity / (DrawHeight * TriangleScale); for (int i = 0; i < parts.Count; i++) { @@ -185,7 +175,7 @@ namespace osu.Game.Graphics.Backgrounds // Limited by the maximum size of QuadVertexBuffer for safety. const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2); - AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio)); + AimCount = (int)Math.Min(max_triangles, DrawWidth * DrawHeight * 0.002f / (TriangleScale * TriangleScale) * SpawnRatio); for (int i = 0; i < AimCount - parts.Count; i++) parts.Add(createTriangle(randomY)); @@ -214,7 +204,7 @@ namespace osu.Game.Graphics.Backgrounds float u1 = 1 - nextRandom(); //uniform(0,1] random floats float u2 = 1 - nextRandom(); float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1) - float scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2) + float scale = Math.Max(TriangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2) return new TriangleParticle { Scale = scale }; } From c7dc6db124653d628860137d40c7c1c419af9133 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 15 Nov 2022 11:32:05 +0300 Subject: [PATCH 083/152] Fix incorrect number of added triangles --- osu.Game/Graphics/Backgrounds/Triangles.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index b5e77ec638..433b264468 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -177,7 +177,9 @@ namespace osu.Game.Graphics.Backgrounds AimCount = (int)Math.Min(max_triangles, DrawWidth * DrawHeight * 0.002f / (TriangleScale * TriangleScale) * SpawnRatio); - for (int i = 0; i < AimCount - parts.Count; i++) + int currentCount = parts.Count; + + for (int i = 0; i < AimCount - currentCount; i++) parts.Add(createTriangle(randomY)); } From f27a5f977d91bc57f40a9f0316f0ec70aae07df5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 15 Nov 2022 11:49:53 +0300 Subject: [PATCH 084/152] Improve triangles distribution --- osu.Game/Graphics/Backgrounds/Triangles.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 433b264468..e2434bb665 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -187,13 +187,27 @@ namespace osu.Game.Graphics.Backgrounds { TriangleParticle particle = CreateTriangle(); - particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1); + particle.Position = getRandomPosition(randomY, particle.Scale); particle.ColourShade = nextRandom(); particle.Colour = CreateTriangleShade(particle.ColourShade); return particle; } + private Vector2 getRandomPosition(bool randomY, float scale) + { + float y = 1; + + if (randomY) + { + // since triangles are drawn from the top - allow them to be positioned a bit above the screen + float maxOffset = triangle_size * scale * 0.866f / DrawHeight; + y = Interpolation.ValueAt(nextRandom(), -maxOffset, 1f, 0f, 1f); + } + + return new Vector2(nextRandom(), y); + } + /// /// Creates a triangle particle with a random scale. /// From 233d45e18508eac8e5544534cdb65f773d02be9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Nov 2022 18:22:27 +0900 Subject: [PATCH 085/152] Fix argon swells incorrectly flashing on every hit --- .../Skinning/Argon/ArgonCirclePiece.cs | 8 ++++++-- osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index 22f96da61e..c22c0e9e79 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Argon @@ -81,12 +82,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon updateStateTransforms(drawableHitObject, drawableHitObject.State.Value); } - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + private void updateStateTransforms(DrawableHitObject h, ArmedState state) { + if (h.HitObject is not Hit) + return; + switch (state) { case ArmedState.Hit: - using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(h.HitStateUpdateTime)) { flash.FadeTo(0.9f).FadeOut(500, Easing.OutQuint); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index ccde8e6ac9..7ddc413d98 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -153,12 +153,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default updateStateTransforms(drawableHitObject, drawableHitObject.State.Value); } - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + private void updateStateTransforms(DrawableHitObject h, ArmedState state) { + if (h.HitObject is not Hit) + return; + switch (state) { case ArmedState.Hit: - using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(h.HitStateUpdateTime)) flashBox.FadeTo(0.9f).FadeOut(300); break; } From e8d170e7728ac7427dcb483a7969dfcc8ceeddac Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Tue, 15 Nov 2022 20:55:55 +0800 Subject: [PATCH 086/152] disable certain options when no beatmap is selected --- osu.Game/Screens/Select/PlaySongSelect.cs | 12 +++++++++++- osu.Game/Screens/Select/SongSelect.cs | 22 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 94e4215175..9c590114d5 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Options; using osu.Game.Users; using osu.Game.Utils; using osuTK.Input; @@ -33,10 +34,12 @@ namespace osu.Game.Screens.Select private PlayBeatmapDetailArea playBeatmapDetailArea = null!; + protected BeatmapOptionsButton? editOptionButton; + [BackgroundDependencyLoader] private void load(OsuColour colours) { - BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); + editOptionButton = BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); } protected void PresentScore(ScoreInfo score) => @@ -142,5 +145,12 @@ namespace osu.Game.Screens.Select playerLoader = null; } } + + protected override void OnBeatmapOptionsButtonDisabledChanged(bool disabled) + { + base.OnBeatmapOptionsButtonDisabledChanged(disabled); + + if (editOptionButton != null) editOptionButton.Disabled = disabled; + } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 1add51e725..72645c23be 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -116,6 +116,10 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; + protected BeatmapOptionsButton deleteOptionButton; + + protected BeatmapOptionsButton clearOptionButton; + [CanBeNull] private IDisposable modSelectOverlayRegistration; @@ -285,9 +289,9 @@ namespace osu.Game.Screens.Select Footer.AddButton(button, overlay); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); - BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); - BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); + deleteOptionButton = BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); + clearOptionButton = BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); } sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); @@ -412,7 +416,13 @@ namespace osu.Game.Screens.Select private void updateCarouselSelection(ValueChangedEvent e = null) { var beatmap = e?.NewValue ?? Beatmap.Value; - if (beatmap is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; + + if (beatmap is DummyWorkingBeatmap || !this.IsCurrentScreen()) + { + OnBeatmapOptionsButtonDisabledChanged(true); + return; + } + OnBeatmapOptionsButtonDisabledChanged(false); Logger.Log($"Song select working beatmap updated to {beatmap}"); @@ -647,6 +657,12 @@ namespace osu.Game.Screens.Select return false; } + protected virtual void OnBeatmapOptionsButtonDisabledChanged(bool disabled) + { + deleteOptionButton.Disabled = disabled; + clearOptionButton.Disabled = disabled; + } + private void playExitingTransition() { ModSelect.Hide(); From 1186ed3e32acad13312db54a5a18ee0cf56257c0 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Tue, 15 Nov 2022 20:56:18 +0800 Subject: [PATCH 087/152] add disable --- .../Select/Options/BeatmapOptionsButton.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 69800c4e86..7d9792d950 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -31,6 +31,31 @@ namespace osu.Game.Screens.Select.Options private readonly OsuSpriteText secondLine; private readonly Container box; + private readonly float disabledAlpha = 0.5f; + + private bool disabled; + + public bool Disabled + { + get => disabled; + set + { + disabled = value; + + if (disabled) + { + firstLine.Alpha = disabledAlpha; + secondLine.Alpha = disabledAlpha; + iconText.Alpha = disabledAlpha; + return; + } + + firstLine.Alpha = 1; + secondLine.Alpha = 1; + iconText.Alpha = 1; + } + } + public Color4 ButtonColour { get => background.Colour; @@ -57,18 +82,24 @@ namespace osu.Game.Screens.Select.Options protected override bool OnMouseDown(MouseDownEvent e) { + if (disabled) return true; + flash.FadeTo(0.1f, 1000, Easing.OutQuint); return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) { + if (disabled) return; + flash.FadeTo(0, 1000, Easing.OutQuint); base.OnMouseUp(e); } protected override bool OnClick(ClickEvent e) { + if (disabled) return true; + flash.ClearTransforms(); flash.Alpha = 0.9f; flash.FadeOut(800, Easing.OutExpo); From 5973bb1956ae05fc8bb73921feb84acb1d3119b7 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Tue, 15 Nov 2022 20:57:31 +0800 Subject: [PATCH 088/152] AddButton now returns the button --- osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 8785dac0aa..139d9e60b5 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select.Options /// Colour of the button. /// Icon of the button. /// Binding the button does. - public void AddButton(LocalisableString firstLine, string secondLine, IconUsage icon, Color4 colour, Action action) + public BeatmapOptionsButton AddButton(LocalisableString firstLine, string secondLine, IconUsage icon, Color4 colour, Action action) { var button = new BeatmapOptionsButton { @@ -82,6 +82,8 @@ namespace osu.Game.Screens.Select.Options }; buttonsContainer.Add(button); + + return button; } protected override void PopIn() From 7aec5ca1e8586b2d14e4deac47ceb79ff2a3a574 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Tue, 15 Nov 2022 20:57:42 +0800 Subject: [PATCH 089/152] visual tests --- .../SongSelect/TestScenePlaySongSelect.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 63532fdba8..db49afe79d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -37,6 +37,7 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; +using osu.Game.Screens.Select.Options; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -1055,6 +1056,24 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("mod overlay hidden", () => songSelect!.ModSelect.State.Value == Visibility.Hidden); } + [Test] + public void TestBeatmapOptionsButtonDisable() + { + createSongSelect(); + + addRulesetImportStep(0); + + AddAssert("delete option enabled", () => songSelect!.DeleteOptionButton.Disabled == false); + AddAssert("clear option enabled", () => songSelect!.ClearOptionButton.Disabled == false); + AddAssert("edit option enabled", () => songSelect!.EditOptionButton?.Disabled == false); + + AddStep("delete all beatmaps", () => manager.Delete()); + + AddAssert("delete option disabled", () => songSelect!.DeleteOptionButton.Disabled == true); + AddAssert("clear option disabled", () => songSelect!.ClearOptionButton.Disabled == true); + AddAssert("edit option disabled", () => songSelect!.EditOptionButton?.Disabled == true); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); @@ -1142,6 +1161,12 @@ namespace osu.Game.Tests.Visual.SongSelect public new BeatmapCarousel Carousel => base.Carousel; public new ModSelectOverlay ModSelect => base.ModSelect; + public BeatmapOptionsButton DeleteOptionButton => base.deleteOptionButton; + + public BeatmapOptionsButton ClearOptionButton => base.clearOptionButton; + + public BeatmapOptionsButton? EditOptionButton => base.editOptionButton; + public new void PresentScore(ScoreInfo score) => base.PresentScore(score); protected override bool OnStart() From 52ecd894db8ca4b44758c5a28ac6ccf2d54ee760 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Nov 2022 22:20:08 +0900 Subject: [PATCH 090/152] Move equilateral triangle ratio to constant --- osu.Game/Graphics/Backgrounds/Triangles.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index e2434bb665..09d137011c 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -26,6 +26,11 @@ namespace osu.Game.Graphics.Backgrounds private const float triangle_size = 100; private const float base_velocity = 50; + /// + /// sqrt(3) / 2 + /// + private const float equilateral_triangle_ratio = 0.866f; + /// /// How many screen-space pixels are smoothed over. /// Same behavior as Sprite's EdgeSmoothness. @@ -149,7 +154,7 @@ namespace osu.Game.Graphics.Backgrounds parts[i] = newParticle; - float bottomPos = parts[i].Position.Y + triangle_size * parts[i].Scale * 0.866f / DrawHeight; + float bottomPos = parts[i].Position.Y + triangle_size * parts[i].Scale * equilateral_triangle_ratio / DrawHeight; if (bottomPos < 0) parts.RemoveAt(i); } @@ -201,7 +206,7 @@ namespace osu.Game.Graphics.Backgrounds if (randomY) { // since triangles are drawn from the top - allow them to be positioned a bit above the screen - float maxOffset = triangle_size * scale * 0.866f / DrawHeight; + float maxOffset = triangle_size * scale * equilateral_triangle_ratio / DrawHeight; y = Interpolation.ValueAt(nextRandom(), -maxOffset, 1f, 0f, 1f); } @@ -290,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds foreach (TriangleParticle particle in parts) { - var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * 0.866f); + var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * equilateral_triangle_ratio); var triangle = new Triangle( Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix), From 70f50c1319f13a63bcbccc2b2e50d13b4d59d174 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Nov 2022 22:46:20 +0900 Subject: [PATCH 091/152] Add test scene covering taiko swell --- .../Skinning/TestSceneDrawableSwell.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs new file mode 100644 index 0000000000..b8c0f6f11e --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneDrawableSwell : TaikoSkinnableTestScene + { + [Test] + public void TestHits() + { + AddStep("Centre hit", () => SetContents(_ => new DrawableSwell(createHitAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + } + + private Swell createHitAtCurrentTime() + { + var hit = new Swell + { + StartTime = Time.Current + 3000, + EndTime = Time.Current + 6000, + }; + + hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return hit; + } + } +} From 9101ad0cbd399f4faa7957792e5efe5d31811932 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 16 Nov 2022 09:42:40 +0800 Subject: [PATCH 092/152] fixed naming --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 6 +++--- osu.Game/Screens/Select/PlaySongSelect.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index db49afe79d..1892eaaf3b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1161,11 +1161,11 @@ namespace osu.Game.Tests.Visual.SongSelect public new BeatmapCarousel Carousel => base.Carousel; public new ModSelectOverlay ModSelect => base.ModSelect; - public BeatmapOptionsButton DeleteOptionButton => base.deleteOptionButton; + public new BeatmapOptionsButton DeleteOptionButton => base.DeleteOptionButton; - public BeatmapOptionsButton ClearOptionButton => base.clearOptionButton; + public new BeatmapOptionsButton ClearOptionButton => base.ClearOptionButton; - public BeatmapOptionsButton? EditOptionButton => base.editOptionButton; + public new BeatmapOptionsButton? EditOptionButton => base.EditOptionButton; public new void PresentScore(ScoreInfo score) => base.PresentScore(score); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 9c590114d5..e61c0c4c59 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -34,12 +34,12 @@ namespace osu.Game.Screens.Select private PlayBeatmapDetailArea playBeatmapDetailArea = null!; - protected BeatmapOptionsButton? editOptionButton; + protected BeatmapOptionsButton? EditOptionButton; [BackgroundDependencyLoader] private void load(OsuColour colours) { - editOptionButton = BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); + EditOptionButton = BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); } protected void PresentScore(ScoreInfo score) => @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Select { base.OnBeatmapOptionsButtonDisabledChanged(disabled); - if (editOptionButton != null) editOptionButton.Disabled = disabled; + if (EditOptionButton != null) EditOptionButton.Disabled = disabled; } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 72645c23be..c91912ec85 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -116,9 +116,9 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; - protected BeatmapOptionsButton deleteOptionButton; + protected BeatmapOptionsButton DeleteOptionButton; - protected BeatmapOptionsButton clearOptionButton; + protected BeatmapOptionsButton ClearOptionButton; [CanBeNull] private IDisposable modSelectOverlayRegistration; @@ -290,8 +290,8 @@ namespace osu.Game.Screens.Select BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); - deleteOptionButton = BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); - clearOptionButton = BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); + DeleteOptionButton = BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); + ClearOptionButton = BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); } sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); @@ -659,8 +659,8 @@ namespace osu.Game.Screens.Select protected virtual void OnBeatmapOptionsButtonDisabledChanged(bool disabled) { - deleteOptionButton.Disabled = disabled; - clearOptionButton.Disabled = disabled; + DeleteOptionButton.Disabled = disabled; + ClearOptionButton.Disabled = disabled; } private void playExitingTransition() From 46c7451ede328a01cb6465ad144f7545cd524f24 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 16 Nov 2022 09:57:23 +0800 Subject: [PATCH 093/152] remove redundant code --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 1892eaaf3b..51c4c5e661 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1063,14 +1063,14 @@ namespace osu.Game.Tests.Visual.SongSelect addRulesetImportStep(0); - AddAssert("delete option enabled", () => songSelect!.DeleteOptionButton.Disabled == false); - AddAssert("clear option enabled", () => songSelect!.ClearOptionButton.Disabled == false); + AddAssert("delete option enabled", () => !songSelect!.DeleteOptionButton.Disabled); + AddAssert("clear option enabled", () => !songSelect!.ClearOptionButton.Disabled); AddAssert("edit option enabled", () => songSelect!.EditOptionButton?.Disabled == false); AddStep("delete all beatmaps", () => manager.Delete()); - AddAssert("delete option disabled", () => songSelect!.DeleteOptionButton.Disabled == true); - AddAssert("clear option disabled", () => songSelect!.ClearOptionButton.Disabled == true); + AddAssert("delete option disabled", () => songSelect!.DeleteOptionButton.Disabled); + AddAssert("clear option disabled", () => songSelect!.ClearOptionButton.Disabled); AddAssert("edit option disabled", () => songSelect!.EditOptionButton?.Disabled == true); } From 1fb8357e93e92cf08d17604f3cbba26a2a8167ff Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 16 Nov 2022 09:58:32 +0800 Subject: [PATCH 094/152] change `disabledAlpha` from `readonly` to `const` --- osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 7d9792d950..1fc5b2bc74 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Select.Options private readonly OsuSpriteText secondLine; private readonly Container box; - private readonly float disabledAlpha = 0.5f; + private const float disabledAlpha = 0.5f; private bool disabled; From 55edd6c9078b0d17a1b68de387f3f4727eb120e7 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 16 Nov 2022 09:58:49 +0800 Subject: [PATCH 095/152] add space --- osu.Game/Screens/Select/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c91912ec85..0341a9bb07 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -422,6 +422,7 @@ namespace osu.Game.Screens.Select OnBeatmapOptionsButtonDisabledChanged(true); return; } + OnBeatmapOptionsButtonDisabledChanged(false); Logger.Log($"Song select working beatmap updated to {beatmap}"); From 5d8d8ffce506d469ca05ea91ccdad4bffb2ff649 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Wed, 16 Nov 2022 10:09:51 +0800 Subject: [PATCH 096/152] fix naming --- osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 1fc5b2bc74..73bc68f6b0 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Select.Options private readonly OsuSpriteText secondLine; private readonly Container box; - private const float disabledAlpha = 0.5f; + private const float disabled_alpha = 0.5f; private bool disabled; @@ -44,9 +44,9 @@ namespace osu.Game.Screens.Select.Options if (disabled) { - firstLine.Alpha = disabledAlpha; - secondLine.Alpha = disabledAlpha; - iconText.Alpha = disabledAlpha; + firstLine.Alpha = disabled_alpha; + secondLine.Alpha = disabled_alpha; + iconText.Alpha = disabled_alpha; return; } From bb762d813150b7d0a89d5e851a719b574f2bae1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Nov 2022 15:45:26 +0900 Subject: [PATCH 097/152] Fix "reset to full area" button not always working correctly --- .../Overlays/Settings/Sections/Input/TabletSettings.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 59c7ff04a2..de52a1f938 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -111,9 +111,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) { t.NewLine(); - var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows - ? @"https://opentabletdriver.net/Wiki/FAQ/Windows" - : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value); + var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription( + RuntimeInfo.OS == RuntimeInfo.Platform.Windows + ? @"https://opentabletdriver.net/Wiki/FAQ/Windows" + : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value); t.AddLinks(formattedSource.Text, formattedSource.Links); } }), @@ -274,6 +275,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input sizeY.Default = sizeY.MaxValue = tab.Size.Y; areaSize.Default = new Vector2(sizeX.Default, sizeY.Default); + areaOffset.Default = new Vector2(offsetX.Default, offsetY.Default); }), true); } From 45f47cce77358ee53b5478e2508eef2fc6287a3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Nov 2022 16:13:52 +0900 Subject: [PATCH 098/152] Add basic osu!taiko "argon" swell visual --- .../Skinning/Argon/ArgonSwellCirclePiece.cs | 34 +++++++++++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 3 ++ 2 files changed, 37 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs new file mode 100644 index 0000000000..82a6e34128 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwellCirclePiece.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public class ArgonSwellCirclePiece : ArgonCirclePiece + { + [BackgroundDependencyLoader] + private void load() + { + AccentColour = ColourInfo.GradientVertical( + new Color4(240, 201, 0, 255), + new Color4(167, 139, 0, 255) + ); + + AddInternal(new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Asterisk, + Size = new Vector2(ICON_SIZE), + Scale = new Vector2(0.8f, 1) + }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index 6d1087793d..a5d091a1c8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionMiss: case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); + + case TaikoSkinComponents.Swell: + return new ArgonSwellCirclePiece(); } break; From ee6fffec5f08bb6d34cae5e7fefb7184eae4132a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Nov 2022 17:54:49 +0900 Subject: [PATCH 099/152] Fix combo colour normalisation setting not applying to editor test play --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 94975b6b5e..7fc62b3c14 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -1,23 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Screens.Play; namespace osu.Game.Screens.Edit.GameplayTest { - public class EditorPlayer : Player + public class EditorPlayer : Player, IGameplaySettings { private readonly Editor editor; private readonly EditorState editorState; [Resolved] - private MusicController musicController { get; set; } + private MusicController musicController { get; set; } = null!; + + private OsuConfigManager config = null!; public EditorPlayer(Editor editor) : base(new PlayerConfiguration { ShowResults = false }) @@ -26,6 +28,14 @@ namespace osu.Game.Screens.Edit.GameplayTest editorState = editor.GetState(); } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + // needs to be populated before BDL to work correctly. + config = parent.Get(); + + return base.CreateChildDependencies(parent); + } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { var masterGameplayClockContainer = new MasterGameplayClockContainer(beatmap, gameplayStart); @@ -74,5 +84,9 @@ namespace osu.Game.Screens.Edit.GameplayTest editor.RestoreState(editorState); return base.OnExiting(e); } + + // Editor overrides but we actually want to use game-wide settings here. + public IBindable ComboColourNormalisationAmount => ((IGameplaySettings)config).ComboColourNormalisationAmount; + public IBindable PositionalHitsoundsLevel => ((IGameplaySettings)config).PositionalHitsoundsLevel; } } From 7a28a7f2a0d7c337a88d8fad0d4d2b721040fe25 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Nov 2022 01:32:54 +0300 Subject: [PATCH 100/152] Move `IGameplaySettings` override to compose screen to keep test mode unaffected --- osu.Game/Configuration/IGameplaySettings.cs | 2 +- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 13 ++++++++++++- osu.Game/Screens/Edit/Editor.cs | 12 +----------- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 18 +----------------- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/osu.Game/Configuration/IGameplaySettings.cs b/osu.Game/Configuration/IGameplaySettings.cs index a35bdd20d0..8d66535017 100644 --- a/osu.Game/Configuration/IGameplaySettings.cs +++ b/osu.Game/Configuration/IGameplaySettings.cs @@ -8,7 +8,7 @@ namespace osu.Game.Configuration { /// /// A settings provider which generally sources from (global user settings) - /// but can allow overriding settings by caching more locally. For instance, in the editor. + /// but can allow overriding settings by caching more locally. For instance, in the editor compose screen. /// /// /// More settings can be moved into this interface as required. diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 3d18b00e75..d3c330c6d7 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.IO.Serialization; using osu.Game.Rulesets; @@ -19,7 +20,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit.Compose { - public class ComposeScreen : EditorScreenWithTimeline + public class ComposeScreen : EditorScreenWithTimeline, IGameplaySettings { [Resolved] private GameHost host { get; set; } @@ -27,6 +28,9 @@ namespace osu.Game.Screens.Edit.Compose [Resolved] private EditorClock clock { get; set; } + [Resolved] + private IGameplaySettings globalGameplaySettings { get; set; } + private Bindable clipboard { get; set; } private HitObjectComposer composer; @@ -157,5 +161,12 @@ namespace osu.Game.Screens.Edit.Compose } #endregion + + // Combo colour normalisation should not be applied in the editor. + // Note this doesn't affect editor test mode. + IBindable IGameplaySettings.ComboColourNormalisationAmount => new Bindable(); + + // Arguable. + IBindable IGameplaySettings.PositionalHitsoundsLevel => globalGameplaySettings.PositionalHitsoundsLevel; } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bb390dfbf3..4c44117581 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -58,8 +58,7 @@ namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] [Cached] - public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider, - IGameplaySettings + public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler, IBeatSyncProvider { public override float BackgroundParallaxAmount => 0.1f; @@ -99,9 +98,6 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } - [Resolved] - private IGameplaySettings globalGameplaySettings { get; set; } - public readonly Bindable Mode = new Bindable(); public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; @@ -1045,11 +1041,5 @@ namespace osu.Game.Screens.Edit { } } - - // Combo colour normalisation should not be applied in the editor. - IBindable IGameplaySettings.ComboColourNormalisationAmount => new Bindable(); - - // Arguable. - IBindable IGameplaySettings.PositionalHitsoundsLevel => globalGameplaySettings.PositionalHitsoundsLevel; } } diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 7fc62b3c14..393ed4ef2e 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -2,16 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Screens.Play; namespace osu.Game.Screens.Edit.GameplayTest { - public class EditorPlayer : Player, IGameplaySettings + public class EditorPlayer : Player { private readonly Editor editor; private readonly EditorState editorState; @@ -19,8 +17,6 @@ namespace osu.Game.Screens.Edit.GameplayTest [Resolved] private MusicController musicController { get; set; } = null!; - private OsuConfigManager config = null!; - public EditorPlayer(Editor editor) : base(new PlayerConfiguration { ShowResults = false }) { @@ -28,14 +24,6 @@ namespace osu.Game.Screens.Edit.GameplayTest editorState = editor.GetState(); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - // needs to be populated before BDL to work correctly. - config = parent.Get(); - - return base.CreateChildDependencies(parent); - } - protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { var masterGameplayClockContainer = new MasterGameplayClockContainer(beatmap, gameplayStart); @@ -84,9 +72,5 @@ namespace osu.Game.Screens.Edit.GameplayTest editor.RestoreState(editorState); return base.OnExiting(e); } - - // Editor overrides but we actually want to use game-wide settings here. - public IBindable ComboColourNormalisationAmount => ((IGameplaySettings)config).ComboColourNormalisationAmount; - public IBindable PositionalHitsoundsLevel => ((IGameplaySettings)config).PositionalHitsoundsLevel; } } From 0e46614c573cb45a0d08dee817c6f82798b9c9fd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Nov 2022 03:54:35 +0300 Subject: [PATCH 101/152] Revert beatmap option button state changes --- .../SongSelect/TestScenePlaySongSelect.cs | 7 ----- .../Select/Options/BeatmapOptionsButton.cs | 29 ------------------- .../Select/Options/BeatmapOptionsOverlay.cs | 4 +-- osu.Game/Screens/Select/PlaySongSelect.cs | 11 +------ osu.Game/Screens/Select/SongSelect.cs | 22 ++------------ 5 files changed, 4 insertions(+), 69 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 51c4c5e661..614ecca6d2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1063,15 +1063,8 @@ namespace osu.Game.Tests.Visual.SongSelect addRulesetImportStep(0); - AddAssert("delete option enabled", () => !songSelect!.DeleteOptionButton.Disabled); - AddAssert("clear option enabled", () => !songSelect!.ClearOptionButton.Disabled); - AddAssert("edit option enabled", () => songSelect!.EditOptionButton?.Disabled == false); - AddStep("delete all beatmaps", () => manager.Delete()); - AddAssert("delete option disabled", () => songSelect!.DeleteOptionButton.Disabled); - AddAssert("clear option disabled", () => songSelect!.ClearOptionButton.Disabled); - AddAssert("edit option disabled", () => songSelect!.EditOptionButton?.Disabled == true); } private void waitForInitialSelection() diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 73bc68f6b0..f41ee63a51 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -33,29 +33,6 @@ namespace osu.Game.Screens.Select.Options private const float disabled_alpha = 0.5f; - private bool disabled; - - public bool Disabled - { - get => disabled; - set - { - disabled = value; - - if (disabled) - { - firstLine.Alpha = disabled_alpha; - secondLine.Alpha = disabled_alpha; - iconText.Alpha = disabled_alpha; - return; - } - - firstLine.Alpha = 1; - secondLine.Alpha = 1; - iconText.Alpha = 1; - } - } - public Color4 ButtonColour { get => background.Colour; @@ -82,24 +59,18 @@ namespace osu.Game.Screens.Select.Options protected override bool OnMouseDown(MouseDownEvent e) { - if (disabled) return true; - flash.FadeTo(0.1f, 1000, Easing.OutQuint); return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) { - if (disabled) return; - flash.FadeTo(0, 1000, Easing.OutQuint); base.OnMouseUp(e); } protected override bool OnClick(ClickEvent e) { - if (disabled) return true; - flash.ClearTransforms(); flash.Alpha = 0.9f; flash.FadeOut(800, Easing.OutExpo); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 139d9e60b5..8785dac0aa 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select.Options /// Colour of the button. /// Icon of the button. /// Binding the button does. - public BeatmapOptionsButton AddButton(LocalisableString firstLine, string secondLine, IconUsage icon, Color4 colour, Action action) + public void AddButton(LocalisableString firstLine, string secondLine, IconUsage icon, Color4 colour, Action action) { var button = new BeatmapOptionsButton { @@ -82,8 +82,6 @@ namespace osu.Game.Screens.Select.Options }; buttonsContainer.Add(button); - - return button; } protected override void PopIn() diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e61c0c4c59..8718e8ad07 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -34,12 +34,10 @@ namespace osu.Game.Screens.Select private PlayBeatmapDetailArea playBeatmapDetailArea = null!; - protected BeatmapOptionsButton? EditOptionButton; - [BackgroundDependencyLoader] private void load(OsuColour colours) { - EditOptionButton = BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); + BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); } protected void PresentScore(ScoreInfo score) => @@ -145,12 +143,5 @@ namespace osu.Game.Screens.Select playerLoader = null; } } - - protected override void OnBeatmapOptionsButtonDisabledChanged(bool disabled) - { - base.OnBeatmapOptionsButtonDisabledChanged(disabled); - - if (EditOptionButton != null) EditOptionButton.Disabled = disabled; - } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 0341a9bb07..062ad17cb1 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -116,10 +116,6 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; - protected BeatmapOptionsButton DeleteOptionButton; - - protected BeatmapOptionsButton ClearOptionButton; - [CanBeNull] private IDisposable modSelectOverlayRegistration; @@ -290,8 +286,8 @@ namespace osu.Game.Screens.Select BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); - DeleteOptionButton = BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); - ClearOptionButton = BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); + BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); + BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); } sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); @@ -417,14 +413,6 @@ namespace osu.Game.Screens.Select { var beatmap = e?.NewValue ?? Beatmap.Value; - if (beatmap is DummyWorkingBeatmap || !this.IsCurrentScreen()) - { - OnBeatmapOptionsButtonDisabledChanged(true); - return; - } - - OnBeatmapOptionsButtonDisabledChanged(false); - Logger.Log($"Song select working beatmap updated to {beatmap}"); if (!Carousel.SelectBeatmap(beatmap.BeatmapInfo, false)) @@ -658,12 +646,6 @@ namespace osu.Game.Screens.Select return false; } - protected virtual void OnBeatmapOptionsButtonDisabledChanged(bool disabled) - { - DeleteOptionButton.Disabled = disabled; - ClearOptionButton.Disabled = disabled; - } - private void playExitingTransition() { ModSelect.Hide(); From 039ab83a46eefad0a61d7dff787c389eef07f38d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Nov 2022 03:57:27 +0300 Subject: [PATCH 102/152] Disable beatmap options button when none selected --- .../SongSelect/TestScenePlaySongSelect.cs | 12 ++---- .../SongSelect/TestSceneSongSelectFooter.cs | 8 ++++ osu.Game/Screens/Select/Footer.cs | 14 ++++++- osu.Game/Screens/Select/FooterButton.cs | 41 ++++++++++++++++--- osu.Game/Screens/Select/PlaySongSelect.cs | 1 - osu.Game/Screens/Select/SongSelect.cs | 14 ++++++- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 614ecca6d2..b6b9e8926b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -37,7 +37,6 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; -using osu.Game.Screens.Select.Options; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -1057,14 +1056,15 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestBeatmapOptionsButtonDisable() + public void TestBeatmapOptionsDisabled() { createSongSelect(); addRulesetImportStep(0); + AddAssert("options enabled", () => songSelect.ChildrenOfType().Single().Enabled.Value); AddStep("delete all beatmaps", () => manager.Delete()); - + AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value); } private void waitForInitialSelection() @@ -1154,12 +1154,6 @@ namespace osu.Game.Tests.Visual.SongSelect public new BeatmapCarousel Carousel => base.Carousel; public new ModSelectOverlay ModSelect => base.ModSelect; - public new BeatmapOptionsButton DeleteOptionButton => base.DeleteOptionButton; - - public new BeatmapOptionsButton ClearOptionButton => base.ClearOptionButton; - - public new BeatmapOptionsButton? EditOptionButton => base.EditOptionButton; - public new void PresentScore(ScoreInfo score) => base.PresentScore(score); protected override bool OnStart() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs index cb78fbfe35..0a88abface 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectFooter.cs @@ -3,8 +3,10 @@ #nullable disable +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Screens.Select; using osuTK; using osuTK.Input; @@ -43,6 +45,12 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.MoveMouseTo(Vector2.Zero); }); + [Test] + public void TestState() + { + AddRepeatStep("toggle options state", () => this.ChildrenOfType().Last().Enabled.Toggle(), 20); + } + [Test] public void TestFooterRandom() { diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 86fe76c0c6..f9fc2890b0 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -57,7 +57,18 @@ namespace osu.Game.Screens.Select } } - private void updateModeLight() => modeLight.FadeColour(buttons.FirstOrDefault(b => b.IsHovered)?.SelectedColour ?? Color4.Transparent, TRANSITION_LENGTH, Easing.OutQuint); + private void updateModeLight() + { + var selectedButton = buttons.FirstOrDefault(b => b.Enabled.Value && b.IsHovered); + + if (selectedButton != null) + { + modeLight.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + modeLight.FadeColour(selectedButton.SelectedColour, TRANSITION_LENGTH, Easing.OutQuint); + } + else + modeLight.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + } public Footer() { @@ -78,6 +89,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Height = 3, Position = new Vector2(0, -3), + Colour = Color4.Black, }, new FillFlowContainer { diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 3f8cf2e13a..230cdfc13e 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -120,10 +120,18 @@ namespace osu.Game.Screens.Select }; } + protected override void LoadComplete() + { + base.LoadComplete(); + Enabled.BindValueChanged(_ => updateDisplay(), true); + } + public Action Hovered; public Action HoverLost; public GlobalAction? Hotkey; + private bool mouseDown; + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -140,32 +148,38 @@ namespace osu.Game.Screens.Select protected override bool OnHover(HoverEvent e) { Hovered?.Invoke(); - light.ScaleTo(new Vector2(1, 2), Footer.TRANSITION_LENGTH, Easing.OutQuint); - light.FadeColour(SelectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint); + updateDisplay(); return true; } protected override void OnHoverLost(HoverLostEvent e) { HoverLost?.Invoke(); - light.ScaleTo(new Vector2(1, 1), Footer.TRANSITION_LENGTH, Easing.OutQuint); - light.FadeColour(DeselectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint); + updateDisplay(); } protected override bool OnMouseDown(MouseDownEvent e) { - box.FadeTo(0.3f, Footer.TRANSITION_LENGTH * 2, Easing.OutQuint); + if (!Enabled.Value) + return true; + + mouseDown = true; + updateDisplay(); return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) { - box.FadeOut(Footer.TRANSITION_LENGTH, Easing.OutQuint); + mouseDown = false; + updateDisplay(); base.OnMouseUp(e); } protected override bool OnClick(ClickEvent e) { + if (!Enabled.Value) + return true; + box.ClearTransforms(); box.Alpha = 1; box.FadeOut(Footer.TRANSITION_LENGTH * 3, Easing.OutQuint); @@ -184,5 +198,20 @@ namespace osu.Game.Screens.Select } public virtual void OnReleased(KeyBindingReleaseEvent e) { } + + private void updateDisplay() + { + this.FadeTo(Enabled.Value ? 1 : 0.25f, Footer.TRANSITION_LENGTH, Easing.OutQuint); + + light.ScaleTo(Enabled.Value && IsHovered ? new Vector2(1, 2) : new Vector2(1), Footer.TRANSITION_LENGTH, Easing.OutQuint); + light.FadeColour(Enabled.Value && IsHovered ? SelectedColour : DeselectedColour, Footer.TRANSITION_LENGTH, Easing.OutQuint); + + box.FadeTo(Enabled.Value & mouseDown ? 0.3f : 0f, Footer.TRANSITION_LENGTH * 2, Easing.OutQuint); + + if (Enabled.Value && IsHovered) + Hovered?.Invoke(); + else + HoverLost?.Invoke(); + } } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 8718e8ad07..94e4215175 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Select.Options; using osu.Game.Users; using osu.Game.Utils; using osuTK.Input; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 062ad17cb1..6b0ab2b4cb 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -112,6 +112,8 @@ namespace osu.Game.Screens.Select protected BeatmapDetailArea BeatmapDetails { get; private set; } + private FooterButtonOptions beatmapOptionsButton; + private readonly Bindable decoupledRuleset = new Bindable(); private double audioFeedbackLastPlaybackTime; @@ -314,7 +316,7 @@ namespace osu.Game.Screens.Select NextRandom = () => Carousel.SelectNextRandom(), PreviousRandom = Carousel.SelectPreviousRandom }, null), - (new FooterButtonOptions(), BeatmapOptions) + (beatmapOptionsButton = new FooterButtonOptions(), BeatmapOptions) }; protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); @@ -738,6 +740,16 @@ namespace osu.Game.Screens.Select beatmapInfoWedge.Beatmap = beatmap; BeatmapDetails.Beatmap = beatmap; + + bool beatmapSelected = beatmap is not DummyWorkingBeatmap; + + if (beatmapSelected) + beatmapOptionsButton.Enabled.Value = true; + else + { + beatmapOptionsButton.Enabled.Value = false; + BeatmapOptions.Hide(); + } } private readonly WeakReference lastTrack = new WeakReference(null); From 55beaf5d931d7964348ae5e8e44839ff2c8cbb3e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Nov 2022 04:05:15 +0300 Subject: [PATCH 103/152] Revert buttons order change --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6b0ab2b4cb..57ba751b54 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -287,8 +287,8 @@ namespace osu.Game.Screens.Select Footer.AddButton(button, overlay); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); - BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); + BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); } From 87b4fee10fc3477c2fd7619270f571e84da7a3cd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Nov 2022 04:05:21 +0300 Subject: [PATCH 104/152] Remove leftover constant --- osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index f41ee63a51..69800c4e86 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.Select.Options private readonly OsuSpriteText secondLine; private readonly Container box; - private const float disabled_alpha = 0.5f; - public Color4 ButtonColour { get => background.Colour; From bfcd9e0f45bf3b8f424073aea7da4e5f150e16ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 12:32:35 +0900 Subject: [PATCH 105/152] Don't seek to current editor location when location is close to (or before) the first object --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 393ed4ef2e..251feecf28 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -27,7 +28,12 @@ namespace osu.Game.Screens.Edit.GameplayTest protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { var masterGameplayClockContainer = new MasterGameplayClockContainer(beatmap, gameplayStart); - masterGameplayClockContainer.Reset(editorState.Time); + + // Only reset the time to the current point if the editor is later than the normal start time (and the first object). + // This allows more sane test playing from the start of the beatmap (ie. correctly adding lead-in time). + if (editorState.Time > gameplayStart && editorState.Time > DrawableRuleset.Objects.FirstOrDefault()?.StartTime) + masterGameplayClockContainer.Reset(editorState.Time); + return masterGameplayClockContainer; } From a4e713a61f7ab1b99cbbcfd28e74a0a971c965e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 12:36:24 +0900 Subject: [PATCH 106/152] When returning to the editor after test play, use the original editor time rather than the point of exit --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 393ed4ef2e..e8ba7ad548 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -68,7 +68,6 @@ namespace osu.Game.Screens.Edit.GameplayTest { musicController.Stop(); - editorState.Time = GameplayClockContainer.CurrentTime; editor.RestoreState(editorState); return base.OnExiting(e); } From adab9f0e48d1cc17aa1a8d5f939d76f3881f5905 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 13:34:40 +0900 Subject: [PATCH 107/152] Catch and gracefully handle file/directory enumeration failures during stable import Closes https://github.com/ppy/osu/issues/21214. --- osu.Game/Database/LegacyBeatmapImporter.cs | 47 ++++++++++++++++------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs index 0955461609..c7e6fa2404 100644 --- a/osu.Game/Database/LegacyBeatmapImporter.cs +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -1,11 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.IO; @@ -22,22 +23,42 @@ namespace osu.Game.Database { // make sure the directory exists if (!storage.ExistsDirectory(string.Empty)) - yield break; + return Array.Empty(); - foreach (string directory in storage.GetDirectories(string.Empty)) + List paths = new List(); + + try { - var directoryStorage = storage.GetStorageForDirectory(directory); - - if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any()) + foreach (string directory in storage.GetDirectories(string.Empty)) { - // if a directory doesn't contain files, attempt looking for beatmaps inside of that directory. - // this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615. - foreach (string subDirectory in GetStableImportPaths(directoryStorage)) - yield return subDirectory; + var directoryStorage = storage.GetStorageForDirectory(directory); + + try + { + if (!directoryStorage.GetFiles(string.Empty).ExcludeSystemFileNames().Any()) + { + // if a directory doesn't contain files, attempt looking for beatmaps inside of that directory. + // this is a special behaviour in stable for beatmaps only, see https://github.com/ppy/osu/issues/18615. + foreach (string subDirectory in GetStableImportPaths(directoryStorage)) + paths.Add(subDirectory); + } + else + paths.Add(storage.GetFullPath(directory)); + } + catch (IOException e) + { + // Catch any errors when enumerating files + Logger.Log($"Error when enumerating files in {directoryStorage.GetFullPath(string.Empty)}: {e}"); + } } - else - yield return storage.GetFullPath(directory); } + catch (IOException e) + { + // Catch any errors when enumerating directories + Logger.Log($"Error when enumerating directories in {storage.GetFullPath(string.Empty)}: {e}"); + } + + return paths; } public LegacyBeatmapImporter(IModelImporter importer) From 1050d7da3400ba08ca3a5361b645a53feddd3ccc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 13:36:16 +0900 Subject: [PATCH 108/152] Use more generic exception type (issue reported `AggegateException`) --- osu.Game/Database/LegacyBeatmapImporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs index c7e6fa2404..887797fe2d 100644 --- a/osu.Game/Database/LegacyBeatmapImporter.cs +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -45,14 +45,14 @@ namespace osu.Game.Database else paths.Add(storage.GetFullPath(directory)); } - catch (IOException e) + catch (Exception e) { // Catch any errors when enumerating files Logger.Log($"Error when enumerating files in {directoryStorage.GetFullPath(string.Empty)}: {e}"); } } } - catch (IOException e) + catch (Exception e) { // Catch any errors when enumerating directories Logger.Log($"Error when enumerating directories in {storage.GetFullPath(string.Empty)}: {e}"); From 5ceb7ecc41c761e2e51c0d1d49534f23cbdc788d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Nov 2022 13:56:27 +0900 Subject: [PATCH 109/152] Remove unused using --- osu.Game/Database/LegacyBeatmapImporter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs index 887797fe2d..20add54949 100644 --- a/osu.Game/Database/LegacyBeatmapImporter.cs +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using osu.Framework.IO.Stores; using osu.Framework.Logging; From 12606122e3dd9e10980d548f3c0c3dbd5558e94e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 13:59:07 +0900 Subject: [PATCH 110/152] Fix sliders not correctly taking on full available length after changing curve type --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 94655f3cf7..c7e3516d62 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -248,6 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components break; } + slider.Path.ExpectedDistance.Value = null; piece.ControlPoint.Type = type; } From bd2e0dc82b05fb61b1ed4671f0cc3c3ccb02680b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 15:09:41 +0900 Subject: [PATCH 111/152] Move "keybindings" keyword to correct section Without this, things like tablet settings would show when searching for bindings, even though these settings have nothing to do with key bindings. --- osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs | 4 ++++ osu.Game/Overlays/Settings/Sections/InputSection.cs | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs index b92746a65a..2f4840a384 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs @@ -3,6 +3,8 @@ #nullable disable +using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Localisation; @@ -13,6 +15,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings; + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" }); + public BindingSettings(KeyBindingPanel keyConfig) { Children = new Drawable[] diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index a8fe3d04be..4d75537f6b 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -3,8 +3,6 @@ #nullable disable -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -22,8 +20,6 @@ namespace osu.Game.Overlays.Settings.Sections public override LocalisableString Header => InputSettingsStrings.InputSectionHeader; - public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" }); - public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.Keyboard From d1b21164006dd08ee56ee1d1e935871e73eab543 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 15:23:20 +0900 Subject: [PATCH 112/152] Auto-advance binding for ruleset key bindings --- .../Settings/Sections/Input/KeyBindingRow.cs | 17 ++++++++++++----- .../Sections/Input/KeyBindingsSubsection.cs | 19 ++++++++++++++++++- .../Input/VariantBindingsSubsection.cs | 2 ++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c91a6a48d4..12fd6f0746 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public class KeyBindingRow : Container, IFilterable { + /// + /// Invoked when binding of this row finalises with a change being written. + /// + public Action BindingFinalised { get; set; } + private readonly object action; private readonly IEnumerable bindings; @@ -153,7 +158,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Spacing = new Vector2(5), Children = new Drawable[] { - new CancelButton { Action = finalise }, + new CancelButton { Action = () => finalise(false) }, new ClearButton { Action = clear }, }, }, @@ -240,7 +245,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } if (bindTarget.IsHovered) - finalise(); + finalise(false); // prevent updating bind target before clear button's action else if (!cancelAndClearButtons.Any(b => b.IsHovered)) updateBindTarget(); @@ -377,10 +382,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input return; bindTarget.UpdateKeyCombination(InputKey.None); - finalise(); + finalise(false); } - private void finalise() + private void finalise(bool changedKey = true) { if (bindTarget != null) { @@ -393,6 +398,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) bindTarget = null; + if (changedKey) + BindingFinalised?.Invoke(this); }); } @@ -417,7 +424,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override void OnFocusLost(FocusLostEvent e) { - finalise(); + finalise(false); base.OnFocusLost(e); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 628fe08607..a3f378e3c9 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -19,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public abstract class KeyBindingsSubsection : SettingsSubsection { + /// + /// After a successful binding, automatically select the next binding row to make quickly + /// binding a large set of keys easier on the user. + /// + protected virtual bool AutoAdvanceTarget => false; + protected IEnumerable Defaults; public RulesetInfo Ruleset { get; protected set; } @@ -49,7 +55,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) { AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination) + Defaults = defaultGroup.Select(d => d.KeyCombination), + BindingFinalised = bindingCompleted }); } @@ -58,6 +65,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input Action = () => Children.OfType().ForEach(k => k.RestoreDefaults()) }); } + + private void bindingCompleted(KeyBindingRow sender) + { + if (AutoAdvanceTarget) + { + var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); + if (next != null) + GetContainingInputManager().ChangeFocus(next); + } + } } public class ResetButton : DangerousSettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs index a0f069b3bb..a6f6c28463 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs @@ -8,6 +8,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public class VariantBindingsSubsection : KeyBindingsSubsection { + protected override bool AutoAdvanceTarget => true; + protected override LocalisableString Header { get; } public VariantBindingsSubsection(RulesetInfo ruleset, int variant) From 4773979e52d9a35f284d8da651ed60df8d7c12d5 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Thu, 17 Nov 2022 16:15:34 +0800 Subject: [PATCH 113/152] add condition --- osu.Game/Screens/Select/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 57ba751b54..5d5019567a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -414,6 +414,7 @@ namespace osu.Game.Screens.Select private void updateCarouselSelection(ValueChangedEvent e = null) { var beatmap = e?.NewValue ?? Beatmap.Value; + if (beatmap is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; Logger.Log($"Song select working beatmap updated to {beatmap}"); From dea2a6ed8d0465303f8ca4140f57371f91c99fd9 Mon Sep 17 00:00:00 2001 From: Piggey Date: Thu, 17 Nov 2022 12:12:36 +0100 Subject: [PATCH 114/152] override `sort()` function in `SoloGameplayLeaderboard` --- .../Screens/Play/HUD/GameplayLeaderboard.cs | 6 +----- .../Play/HUD/SoloGameplayLeaderboard.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index c8631880ac..2a47964118 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Play.HUD } } - private void sort() + protected virtual void sort() { if (sorting.IsValid) return; @@ -174,10 +174,6 @@ namespace osu.Game.Screens.Play.HUD orderedByScore[i].ScorePosition = i + 1; } - // change displayed potision to '-' when there are 50 already submitted scores and tracked score is last - if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count == 51) - TrackedScore.ScorePosition = null; - sorting.Validate(); } diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index ab3cf2950c..bf9cdc8db7 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Select; using osu.Game.Users; namespace osu.Game.Screens.Play.HUD @@ -16,6 +17,7 @@ namespace osu.Game.Screens.Play.HUD public class SoloGameplayLeaderboard : GameplayLeaderboard { private const int duration = 100; + private const int max_online_scores = 50; // BAD! private readonly Bindable configVisibility = new Bindable(); private readonly IUser trackingUser; @@ -42,10 +44,15 @@ namespace osu.Game.Screens.Play.HUD this.trackingUser = trackingUser; } + private PlayBeatmapDetailArea.TabType scoresType; + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + + // a way to differentiate scores taken from online ranking to local scores + scoresType = config.Get(OsuSetting.BeatmapDetailTab); } protected override void LoadComplete() @@ -93,6 +100,18 @@ namespace osu.Game.Screens.Play.HUD local.DisplayOrder.Value = long.MaxValue; } + protected override void sort() + { + base.sort(); + + if (scoresType != PlayBeatmapDetailArea.TabType.Local) + { + // change displayed potision to '-' when there are 50 already submitted scores and tracked score is last + if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > max_online_scores) + TrackedScore.ScorePosition = null; + } + } + private void updateVisibility() => this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); } From fa00fc5c6d2ee22f393087a921c7b637f1f3823a Mon Sep 17 00:00:00 2001 From: Piggey Date: Thu, 17 Nov 2022 13:27:56 +0100 Subject: [PATCH 115/152] change scoresType to be a bindable --- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index bf9cdc8db7..39f584efd5 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play.HUD this.trackingUser = trackingUser; } - private PlayBeatmapDetailArea.TabType scoresType; + private Bindable scoresType = new Bindable(); [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Play.HUD config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); // a way to differentiate scores taken from online ranking to local scores - scoresType = config.Get(OsuSetting.BeatmapDetailTab); + scoresType = config.GetBindable(OsuSetting.BeatmapDetailTab); } protected override void LoadComplete() @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Play.HUD { base.sort(); - if (scoresType != PlayBeatmapDetailArea.TabType.Local) + if (scoresType.Value != PlayBeatmapDetailArea.TabType.Local) { // change displayed potision to '-' when there are 50 already submitted scores and tracked score is last if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > max_online_scores) From 5e4ade3bee2ee0c77e49a00d4ce13a014c72dcb3 Mon Sep 17 00:00:00 2001 From: Piggey Date: Thu, 17 Nov 2022 13:28:04 +0100 Subject: [PATCH 116/152] add tests --- .../TestSceneSoloGameplayLeaderboard.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index 60ed0012ae..edda0ab416 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Gameplay { @@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly BindableList scores = new BindableList(); private readonly Bindable configVisibility = new Bindable(); + private readonly Bindable beatmapTabType = new Bindable(); private SoloGameplayLeaderboard leaderboard = null!; @@ -33,6 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void load(OsuConfigManager config) { config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType); } [SetUpSteps] @@ -70,6 +73,25 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value); } + [Test] + public void TestTrackedScorePosition() + { + AddStep("change TabType to global", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Global); + AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition! == 50); + + AddStep("change TabType to local", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Local); + AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition! == 50); + + AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) })); + AddUntilStep("tracked player is #51", () => leaderboard.TrackedScore?.ScorePosition! == 51); + + AddStep("change TabType to global", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Global); + AddUntilStep("tracked player is -", () => leaderboard.TrackedScore?.ScorePosition! == null); + + AddStep("change TabType to country", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Country); + AddUntilStep("tracked player is -", () => leaderboard.TrackedScore?.ScorePosition! == null); + } + [Test] public void TestVisibility() { @@ -86,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); } - private static List createSampleScores() + private static List createSampleScores(int numRandScores = 50) { return new[] { @@ -95,7 +117,7 @@ namespace osu.Game.Tests.Visual.Gameplay new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) }, new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) }, new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) }, - }.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList(); + }.Concat(Enumerable.Range(0, 49 - 5).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList(); } } } From 20d87fbed4c9bc1079a4da3d94a4eb2ada093ad5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 23:05:34 +0900 Subject: [PATCH 117/152] Eagerly dispose of triangles intro textures to avoid holding for full length of game session --- osu.Game/Screens/Menu/IntroTriangles.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 05a6d25303..4ec877b85a 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -224,8 +224,8 @@ namespace osu.Game.Screens.Menu { rulesetsScale.ScaleTo(0.8f, 1000); rulesets.FadeIn().ScaleTo(1).TransformSpacingTo(new Vector2(200, 0)); - welcomeText.FadeOut(); - triangles.FadeOut(); + welcomeText.FadeOut().Expire(); + triangles.FadeOut().Expire(); } using (BeginDelayedSequence(rulesets_2)) @@ -307,7 +307,7 @@ namespace osu.Game.Screens.Menu } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { InternalChildren = new Drawable[] { From a3e8cc1663be4dab2ebfa9cbd59f0f5adec95da5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 23:41:35 +0900 Subject: [PATCH 118/152] Mark beatmap listing placeholder textures as large --- osu.Game/Overlays/BeatmapListingOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index c278c9cb93..b78a432194 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -287,7 +287,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { AddInternal(new FillFlowContainer { @@ -332,7 +332,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { AddInternal(new FillFlowContainer { From 63c185551111aafdcabfc89cd093349ebd420065 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 23:41:49 +0900 Subject: [PATCH 119/152] Avoid loading beatmap listing placeholders until required --- osu.Game/Overlays/BeatmapListingOverlay.cs | 43 ++++------------------ 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index b78a432194..5e922973ec 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -41,11 +41,8 @@ namespace osu.Game.Overlays private IBindable apiUser; - private Drawable currentContent; private Container panelTarget; private FillFlowContainer foundContent; - private NotFoundDrawable notFoundContent; - private SupporterRequiredDrawable supporterRequiredContent; private BeatmapListingFilterControl filterControl; public BeatmapListingOverlay() @@ -86,11 +83,6 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Masking = true, Padding = new MarginPadding { Horizontal = 20 }, - Children = new Drawable[] - { - notFoundContent = new NotFoundDrawable(), - supporterRequiredContent = new SupporterRequiredDrawable(), - } } }, }, @@ -107,7 +99,7 @@ namespace osu.Game.Overlays apiUser.BindValueChanged(_ => Schedule(() => { if (api.IsLoggedIn) - addContentToResultsArea(Drawable.Empty()); + replaceResultsAreaContent(Drawable.Empty()); })); } @@ -155,8 +147,9 @@ namespace osu.Game.Overlays if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters) { - supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed); - addContentToResultsArea(supporterRequiredContent); + var supporterOnly = new SupporterRequiredDrawable(); + supporterOnly.UpdateText(searchResult.SupporterOnlyFiltersUsed); + replaceResultsAreaContent(supporterOnly); return; } @@ -167,13 +160,13 @@ namespace osu.Game.Overlays //No matches case if (!newCards.Any()) { - addContentToResultsArea(notFoundContent); + replaceResultsAreaContent(new NotFoundDrawable()); return; } var content = createCardContainerFor(newCards); - panelLoadTask = LoadComponentAsync(foundContent = content, addContentToResultsArea, (cancellationToken = new CancellationTokenSource()).Token); + panelLoadTask = LoadComponentAsync(foundContent = content, replaceResultsAreaContent, (cancellationToken = new CancellationTokenSource()).Token); } else { @@ -221,36 +214,16 @@ namespace osu.Game.Overlays return content; } - private void addContentToResultsArea(Drawable content) + private void replaceResultsAreaContent(Drawable content) { Loading.Hide(); lastFetchDisplayedTime = Time.Current; - if (content == currentContent) - return; - - var lastContent = currentContent; - - if (lastContent != null) - { - lastContent.FadeOut(); - if (!isPlaceholderContent(lastContent)) - lastContent.Expire(); - } - - if (!content.IsAlive) - panelTarget.Add(content); + panelTarget.Child = content; content.FadeInFromZero(); - currentContent = content; } - /// - /// Whether is a static placeholder reused multiple times by this overlay. - /// - private bool isPlaceholderContent(Drawable drawable) - => drawable == notFoundContent || drawable == supporterRequiredContent; - private void onCardSizeChanged() { if (foundContent?.IsAlive != true || !foundContent.Any()) From 73e4827d36a57e72a8e26a1ab31882939f19a6a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2022 23:55:40 +0900 Subject: [PATCH 120/152] Avoid loading overlay headers until first open Also switches them to use non-atlased target as they won't benefit much from atlasing. --- osu.Game/Overlays/OverlayHeaderBackground.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/OverlayHeaderBackground.cs b/osu.Game/Overlays/OverlayHeaderBackground.cs index c47f16272f..540b28d9b2 100644 --- a/osu.Game/Overlays/OverlayHeaderBackground.cs +++ b/osu.Game/Overlays/OverlayHeaderBackground.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays Height = 80; RelativeSizeAxes = Axes.X; Masking = true; - InternalChild = new Background(textureName); + InternalChild = new DelayedLoadWrapper(() => new Background(textureName)); } private class Background : Sprite @@ -36,10 +36,16 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { Texture = textures.Get(textureName); } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeInFromZero(500, Easing.OutQuint); + } } } } From b37e9c0266088f187329c47b9490eb5e889798fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 00:01:53 +0900 Subject: [PATCH 121/152] Fix news post images never unloading from memory after first display --- .../Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs | 7 +------ osu.Game/Overlays/News/NewsCard.cs | 7 ++----- osu.Game/Overlays/News/NewsPostBackground.cs | 7 +++++++ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs index 8f5c942b6e..1d904526fd 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs @@ -119,22 +119,17 @@ namespace osu.Game.Overlays.Dashboard.Home.News [BackgroundDependencyLoader] private void load(GameHost host) { - NewsPostBackground bg; - - Child = new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage) + Child = new DelayedLoadUnloadWrapper(() => new NewsPostBackground(post.FirstImage) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Alpha = 0 }) { RelativeSizeAxes = Axes.Both }; - bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); - TooltipText = "view in browser"; Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index eb76522e11..c8e0b0c7ef 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -49,7 +49,6 @@ namespace osu.Game.Overlays.News Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); } - NewsPostBackground bg; AddRange(new Drawable[] { background = new Box @@ -71,14 +70,14 @@ namespace osu.Game.Overlays.News CornerRadius = 6, Children = new Drawable[] { - new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage) + new DelayedLoadUnloadWrapper(() => new NewsPostBackground(post.FirstImage) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0 - }) + }, timeBeforeUnload: 5000) { RelativeSizeAxes = Axes.Both }, @@ -116,8 +115,6 @@ namespace osu.Game.Overlays.News IdleColour = colourProvider.Background4; HoverColour = colourProvider.Background3; - bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); - main.AddParagraph(post.Title, t => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold)); main.AddParagraph(post.Preview, t => t.Font = OsuFont.GetFont(size: 12)); // Should use sans-serif font main.AddParagraph("by ", t => t.Font = OsuFont.GetFont(size: 12)); diff --git a/osu.Game/Overlays/News/NewsPostBackground.cs b/osu.Game/Overlays/News/NewsPostBackground.cs index bddca8f7ec..b77623842c 100644 --- a/osu.Game/Overlays/News/NewsPostBackground.cs +++ b/osu.Game/Overlays/News/NewsPostBackground.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -25,6 +26,12 @@ namespace osu.Game.Overlays.News Texture = store.Get(createUrl(sourceUrl)); } + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeInFromZero(500, Easing.OutQuint); + } + private string createUrl(string source) { if (string.IsNullOrEmpty(source)) From eb8f6626abebab7e24ccb8afe34303c3c87f5a06 Mon Sep 17 00:00:00 2001 From: Piggey Date: Thu, 17 Nov 2022 21:38:09 +0100 Subject: [PATCH 122/152] revert changes from `GameplayLeaderboard` tests --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index a385060d62..171ae829a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add many scores in one go", () => { - for (int i = 0; i < 49; i++) + for (int i = 0; i < 32; i++) createRandomScore(new APIUser { Username = $"Player {i + 1}" }); // Add player at end to force an animation down the whole list. @@ -61,12 +61,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); - AddUntilStep("ensure player is #50", () => leaderboard.CheckPositionByUsername("You", 50)); - - AddStep("add one more player", () => createRandomScore(new APIUser { Username = "Player 50" })); - - AddUntilStep("ensure player is #?", () => leaderboard.CheckPositionByUsername("You", null)); - AddStep("change score to middle", () => playerScore.Value = 1000000); AddWaitStep("wait for movement", 5); AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); From 53769479c772acdfe5aadc324a9a9d7e88e9c097 Mon Sep 17 00:00:00 2001 From: Piggey Date: Thu, 17 Nov 2022 21:42:15 +0100 Subject: [PATCH 123/152] oh oops --- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index edda0ab416..fc1a67e285 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); } - private static List createSampleScores(int numRandScores = 50) + private static List createSampleScores() { return new[] { @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Gameplay new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) }, new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) }, new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) }, - }.Concat(Enumerable.Range(0, 49 - 5).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList(); + }.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList(); } } } From eb691266c1e8c9dd71bb44452e9a6871105974d3 Mon Sep 17 00:00:00 2001 From: Piggey Date: Thu, 17 Nov 2022 21:50:39 +0100 Subject: [PATCH 124/152] cleanup --- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index 39f584efd5..bf83c9a3c6 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -39,13 +39,13 @@ namespace osu.Game.Screens.Play.HUD /// public readonly Bindable AlwaysVisible = new Bindable(true); + private Bindable scoresType = new Bindable(); + public SoloGameplayLeaderboard(IUser trackingUser) { this.trackingUser = trackingUser; } - private Bindable scoresType = new Bindable(); - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { From 93c2280754d8bb859eb26914e90fa7e5eaf69313 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 10:35:05 +0900 Subject: [PATCH 125/152] Fix supporter filters text not being set correctly --- osu.Game/Overlays/BeatmapListingOverlay.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5e922973ec..2d9583b864 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -147,8 +147,7 @@ namespace osu.Game.Overlays if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters) { - var supporterOnly = new SupporterRequiredDrawable(); - supporterOnly.UpdateText(searchResult.SupporterOnlyFiltersUsed); + var supporterOnly = new SupporterRequiredDrawable(searchResult.SupporterOnlyFiltersUsed); replaceResultsAreaContent(supporterOnly); return; } @@ -297,11 +296,15 @@ namespace osu.Game.Overlays { private LinkFlowContainer supporterRequiredText; - public SupporterRequiredDrawable() + private readonly List filtersUsed; + + public SupporterRequiredDrawable(List filtersUsed) { RelativeSizeAxes = Axes.X; Height = 225; Alpha = 0; + + this.filtersUsed = filtersUsed; } [BackgroundDependencyLoader] @@ -333,14 +336,9 @@ namespace osu.Game.Overlays }, } }); - } - - public void UpdateText(List filters) - { - supporterRequiredText.Clear(); supporterRequiredText.AddText( - BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(), + BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filtersUsed), "").ToString(), t => { t.Font = OsuFont.GetFont(size: 16); From b53f9baf6a8d034dd36fd3a2d271205119dbb362 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 11:21:12 +0900 Subject: [PATCH 126/152] Fix `NowPlayingOverlay` loading background texture too early (and permanently) --- osu.Game/Overlays/NowPlayingOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 900b4bebf0..949f1e7b96 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -100,7 +100,7 @@ namespace osu.Game.Overlays }, Children = new[] { - background = new Background(), + background = Empty(), title = new OsuSpriteText { Origin = Anchor.BottomCentre, @@ -413,7 +413,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); } From 82829867db1522a7a255b97fbc8c17a1083a84c7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Nov 2022 07:07:56 +0300 Subject: [PATCH 127/152] Fix beatmap options test failure due to no beatmap being selected --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d58887c090..e500efede3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -436,6 +436,8 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); From a5d22195f2bdd39bb030f2536b93af908070131e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 13:45:46 +0900 Subject: [PATCH 128/152] Fix potential incorrect connection state resulting in null reference --- osu.Game/Online/PersistentEndpointClientConnector.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index 2c4e127723..be76644745 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -145,7 +145,9 @@ namespace osu.Game.Online private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken) { - isConnected.Value = false; + bool hasBeenCancelled = cancellationToken.IsCancellationRequested; + + await disconnect(true); if (ex != null) await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false); @@ -153,7 +155,7 @@ namespace osu.Game.Online Logger.Log($"{ClientName} disconnected", LoggingTarget.Network); // make sure a disconnect wasn't triggered (and this is still the active connection). - if (!cancellationToken.IsCancellationRequested) + if (!hasBeenCancelled) await Task.Run(connect, default).ConfigureAwait(false); } @@ -174,7 +176,9 @@ namespace osu.Game.Online } finally { + isConnected.Value = false; CurrentConnection = null; + if (takeLock) connectionLock.Release(); } From 551192b413ab59053383107a336b9a72a98e9df2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 18 Nov 2022 13:55:37 +0900 Subject: [PATCH 129/152] Refactor a bit for readability --- .../Overlays/Settings/Sections/Input/KeyBindingRow.cs | 10 +++++----- .../Settings/Sections/Input/KeyBindingsSubsection.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 12fd6f0746..832aa759d0 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -34,9 +34,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input public class KeyBindingRow : Container, IFilterable { /// - /// Invoked when binding of this row finalises with a change being written. + /// Invoked when the binding of this row is updated with a change being written. /// - public Action BindingFinalised { get; set; } + public Action BindingUpdated { get; set; } private readonly object action; private readonly IEnumerable bindings; @@ -385,7 +385,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input finalise(false); } - private void finalise(bool changedKey = true) + private void finalise(bool hasChanged = true) { if (bindTarget != null) { @@ -398,8 +398,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) bindTarget = null; - if (changedKey) - BindingFinalised?.Invoke(this); + if (hasChanged) + BindingUpdated?.Invoke(this); }); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index a3f378e3c9..98d569948f 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination), - BindingFinalised = bindingCompleted + BindingUpdated = onBindingUpdated }); } @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }); } - private void bindingCompleted(KeyBindingRow sender) + private void onBindingUpdated(KeyBindingRow sender) { if (AutoAdvanceTarget) { From e4d134a82057b3d6f05feab5f6251612488a61d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 14:07:40 +0900 Subject: [PATCH 130/152] Reduce time waited on a score submission token from 60 to 30s --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 345bd5a134..fb296d7988 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play api.Queue(req); // Generally a timeout would not happen here as APIAccess will timeout first. - if (!tcs.Task.Wait(60000)) + if (!tcs.Task.Wait(30000)) req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)")); return true; From 964ceddf83360c24ae0cabc4e895e537237bc2c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 14:21:37 +0900 Subject: [PATCH 131/152] Fix API queue only being flushed once while in a failing state --- osu.Game/Online/API/APIAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8ac2e2d453..65f78e1fd7 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -419,7 +419,7 @@ namespace osu.Game.Online.API failureCount++; log.Add($@"API failure count is now {failureCount}"); - if (failureCount >= 3 && State.Value == APIState.Online) + if (failureCount >= 3) { state.Value = APIState.Failing; flushQueue(); From d47c46d1443be45ad90ca2e70780b21a94aa1080 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 18 Nov 2022 14:16:12 +0900 Subject: [PATCH 132/152] Compute raw score in-line Saves on an allocation (though it seems to only be a ValueType so it shouldn't be a big deal) and a tail-call. --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 2b6e40b94e..899d149cbc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -335,24 +335,21 @@ namespace osu.Game.Rulesets.Scoring [Pure] public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects) { + double accuracyScore = accuracyPortion * accuracyRatio; + double comboScore = comboPortion * comboRatio; + double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + switch (mode) { default: case ScoringMode.Standardised: - return (long)Math.Round(computeRawScore()); + return (long)Math.Round(rawScore); case ScoringMode.Classic: // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. - double scaledStandardised = computeRawScore() / max_score; - return (long)Math.Round(Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); - } - - double computeRawScore() - { - double accuracyScore = accuracyPortion * accuracyRatio; - double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + double scaledRawScore = rawScore / max_score; + return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier); } } From 5fc0d45675475d0932c15f566ba5c131d81ab406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 14:29:10 +0900 Subject: [PATCH 133/152] Fix triangle button flash effect looking incorrect --- osu.Game/Graphics/UserInterface/OsuButton.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 9140815f32..dae5de2d65 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -69,6 +70,8 @@ namespace osu.Game.Graphics.UserInterface protected Box Background; protected SpriteText SpriteText; + private readonly Box flashLayer; + public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button) { Height = 40; @@ -99,6 +102,14 @@ namespace osu.Game.Graphics.UserInterface Depth = float.MinValue }, SpriteText = CreateText(), + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Depth = float.MinValue, + Colour = Color4.White.Opacity(0.5f), + Alpha = 0, + }, } }); @@ -125,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnClick(ClickEvent e) { if (Enabled.Value) - Background.FlashColour(Color4.White, 800, Easing.OutQuint); + flashLayer.FadeOutFromOne(800, Easing.OutQuint); return base.OnClick(e); } From b0faa009c1d6262a08a5f6bd0b0256b6101cfb7c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Nov 2022 11:30:29 +0900 Subject: [PATCH 134/152] Add some debugging for multiplayer test failures --- .../Online/Multiplayer/MultiplayerClient.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 75334952f0..b39781ebc2 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; @@ -727,13 +728,20 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - Debug.Assert(APIRoom != null); + try + { + Debug.Assert(APIRoom != null); - Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item; + Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item; - int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); - APIRoom.Playlist.RemoveAt(existingIndex); - APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item)); + int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID)); + APIRoom.Playlist.RemoveAt(existingIndex); + APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item)); + } + catch (Exception ex) + { + throw new AggregateException($"Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex); + } ItemChanged?.Invoke(item); RoomUpdated?.Invoke(); From 45f5849301c2c8b990a179485ac9a1d1fc470bcb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:33:13 +0900 Subject: [PATCH 135/152] Add more test coverage to `TestSceneKeyBindingPanel` --- .../Settings/TestSceneKeyBindingPanel.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index d7d073e908..99b8be0114 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -40,6 +40,33 @@ namespace osu.Game.Tests.Visual.Settings AddWaitStep("wait for scroll", 5); } + [Test] + public void TestBindingSingleKey() + { + scrollToAndStartBinding("Increase volume"); + AddStep("press k", () => InputManager.Key(Key.K)); + checkBinding("Increase volume", "K"); + } + + [Test] + public void TestBindingSingleModifier() + { + scrollToAndStartBinding("Increase volume"); + AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + checkBinding("Increase volume", "LShift"); + } + + [Test] + public void TestBindingSingleKeyWithModifier() + { + scrollToAndStartBinding("Increase volume"); + AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("press k", () => InputManager.Key(Key.K)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + checkBinding("Increase volume", "LShift-K"); + } + [Test] public void TestBindingMouseWheelToNonGameplay() { @@ -169,7 +196,8 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); - AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + AddAssert("binding cleared", + () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); } [Test] @@ -198,7 +226,8 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); - AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + AddAssert("binding cleared", + () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); } [Test] @@ -256,8 +285,8 @@ namespace osu.Game.Tests.Visual.Settings var firstRow = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == name)); var firstButton = firstRow.ChildrenOfType().First(); - return firstButton.Text.Text == keyName; - }); + return firstButton.Text.Text.ToString(); + }, () => Is.EqualTo(keyName)); } private void scrollToAndStartBinding(string name) From cb8275ee755d23d109e61d164282bc08bf529cfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:34:26 +0900 Subject: [PATCH 136/152] Add failing test coverage to `TestSceneKeyBindingPanel` for multiple non-modifiers being bound --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 99b8be0114..bd54591b9b 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -40,6 +40,16 @@ namespace osu.Game.Tests.Visual.Settings AddWaitStep("wait for scroll", 5); } + [Test] + public void TestBindingTwoNonModifiers() + { + AddStep("press j", () => InputManager.PressKey(Key.J)); + scrollToAndStartBinding("Increase volume"); + AddStep("press k", () => InputManager.Key(Key.K)); + AddStep("release j", () => InputManager.ReleaseKey(Key.J)); + checkBinding("Increase volume", "K"); + } + [Test] public void TestBindingSingleKey() { From e658efbefa9a6452da5b36d894bf6840342ba9f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:20:46 +0900 Subject: [PATCH 137/152] Fix being able to bind two non-modifier keys to the same binding --- .../Settings/Sections/Input/KeyBindingRow.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c91a6a48d4..6ce8a188e6 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -226,7 +226,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMouseButton(e.Button)); return true; } @@ -252,7 +252,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { if (bindTarget.IsHovered) { - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta), KeyCombination.FromScrollDelta(e.ScrollDelta).First()); finalise(); return true; } @@ -263,10 +263,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override bool OnKeyDown(KeyDownEvent e) { - if (!HasFocus) + if (!HasFocus || e.Repeat) return false; - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromKey(e.Key)); if (!isModifier(e.Key)) finalise(); return true; @@ -288,7 +288,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromJoystickButton(e.Button)); finalise(); return true; @@ -310,7 +310,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMidiKey(e.Key)); finalise(); return true; @@ -332,7 +332,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletAuxiliaryButton(e.Button)); finalise(); return true; @@ -354,7 +354,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletPenButton(e.Button)); finalise(); return true; @@ -563,6 +563,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } + /// + /// Update from a key combination, only allowing a single non-modifier key to be specified. + /// + /// A generated from the full input state. + /// The key which triggered this update, and should be used as the binding. + public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) => + UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey))); + public void UpdateKeyCombination(KeyCombination newCombination) { if (KeyBinding.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) From 90cd38632391e799b43cf865d1b8fcad1df54cc3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Nov 2022 09:40:08 +0300 Subject: [PATCH 138/152] Fix timeline potentially scrolling at extents while not dragging --- .../Components/Timeline/TimelineHitObjectBlueprint.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 974e240552..20ef128ee9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -436,8 +436,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.OnDragEnd(e); - OnDragHandled?.Invoke(null); + dragOperation?.Cancel(); + dragOperation = null; + changeHandler?.EndChange(); + OnDragHandled?.Invoke(null); } } From e2aca8dc9073694983188ed10637879f16975b90 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Nov 2022 09:46:50 +0300 Subject: [PATCH 139/152] Suppress nullable warning for now --- osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs index 1df3d336ee..e7fdb52d2f 100644 --- a/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs +++ b/osu.Game.Tests/Database/LegacyBeatmapImporterTest.cs @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Database private class TestLegacyBeatmapImporter : LegacyBeatmapImporter { public TestLegacyBeatmapImporter() - : base(null) + : base(null!) { } From d17c091d639cbf330d9cd1e535ec267074c6c5b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:47:31 +0900 Subject: [PATCH 140/152] Move constant to a better location --- osu.Game/Online/API/Requests/GetScoresRequest.cs | 2 ++ osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index 966e69938c..7d1d26b75d 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -16,6 +16,8 @@ namespace osu.Game.Online.API.Requests { public class GetScoresRequest : APIRequest { + public const int MAX_SCORES_PER_REQUEST = 50; + private readonly IBeatmapInfo beatmapInfo; private readonly BeatmapLeaderboardScope scope; private readonly IRulesetInfo ruleset; diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index bf83c9a3c6..243634ee36 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; +using osu.Game.Online.API.Requests; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Select; @@ -17,7 +18,6 @@ namespace osu.Game.Screens.Play.HUD public class SoloGameplayLeaderboard : GameplayLeaderboard { private const int duration = 100; - private const int max_online_scores = 50; // BAD! private readonly Bindable configVisibility = new Bindable(); private readonly IUser trackingUser; @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Play.HUD if (scoresType.Value != PlayBeatmapDetailArea.TabType.Local) { // change displayed potision to '-' when there are 50 already submitted scores and tracked score is last - if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > max_online_scores) + if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) TrackedScore.ScorePosition = null; } } From e4f522aad1344084857a42659c51a33baea05bc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:47:37 +0900 Subject: [PATCH 141/152] Fix inspections / typos --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 4 ++-- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 2a47964118..7e8c2401dd 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - Scheduler.AddDelayed(sort, 1000, true); + Scheduler.AddDelayed(Sort, 1000, true); } /// @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Play.HUD } } - protected virtual void sort() + protected virtual void Sort() { if (sorting.IsValid) return; diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index 243634ee36..0a2f72121d 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -100,13 +100,13 @@ namespace osu.Game.Screens.Play.HUD local.DisplayOrder.Value = long.MaxValue; } - protected override void sort() + protected override void Sort() { - base.sort(); + base.Sort(); if (scoresType.Value != PlayBeatmapDetailArea.TabType.Local) { - // change displayed potision to '-' when there are 50 already submitted scores and tracked score is last + // change displayed position to '-' when there are 50 already submitted scores and tracked score is last if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) TrackedScore.ScorePosition = null; } From 20af8217f4d738bbe7a8019ba236e928c4ffe170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:50:41 +0900 Subject: [PATCH 142/152] Tidy up bindable flow --- .../Screens/Play/HUD/SoloGameplayLeaderboard.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index 0a2f72121d..e9e015f96d 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Play.HUD private const int duration = 100; private readonly Bindable configVisibility = new Bindable(); + + private readonly Bindable scoreSource = new Bindable(); + private readonly IUser trackingUser; public readonly IBindableList Scores = new BindableList(); @@ -39,8 +42,6 @@ namespace osu.Game.Screens.Play.HUD /// public readonly Bindable AlwaysVisible = new Bindable(true); - private Bindable scoresType = new Bindable(); - public SoloGameplayLeaderboard(IUser trackingUser) { this.trackingUser = trackingUser; @@ -50,14 +51,13 @@ namespace osu.Game.Screens.Play.HUD private void load(OsuConfigManager config) { config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); - - // a way to differentiate scores taken from online ranking to local scores - scoresType = config.GetBindable(OsuSetting.BeatmapDetailTab); + config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource); } protected override void LoadComplete() { base.LoadComplete(); + Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true); // Alpha will be updated via `updateVisibility` below. @@ -104,9 +104,9 @@ namespace osu.Game.Screens.Play.HUD { base.Sort(); - if (scoresType.Value != PlayBeatmapDetailArea.TabType.Local) + // change displayed position to '-' when there are 50 already submitted scores and tracked score is last + if (scoreSource.Value != PlayBeatmapDetailArea.TabType.Local) { - // change displayed position to '-' when there are 50 already submitted scores and tracked score is last if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) TrackedScore.ScorePosition = null; } From aff218dfd528d9900246248a990d2b9312271054 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:52:39 +0900 Subject: [PATCH 143/152] Redirect through validity function rather than overriding `Sort` --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 8 +++++--- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 7e8c2401dd..47b67fba00 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - Scheduler.AddDelayed(Sort, 1000, true); + Scheduler.AddDelayed(sort, 1000, true); } /// @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Play.HUD } } - protected virtual void Sort() + private void sort() { if (sorting.IsValid) return; @@ -171,12 +171,14 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < Flow.Count; i++) { Flow.SetLayoutPosition(orderedByScore[i], i); - orderedByScore[i].ScorePosition = i + 1; + orderedByScore[i].ScorePosition = CheckValidScorePosition(i + 1) ? i + 1 : null; } sorting.Validate(); } + protected virtual bool CheckValidScorePosition(int i) => true; + private class InputDisabledScrollContainer : OsuScrollContainer { public InputDisabledScrollContainer() diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index e9e015f96d..0df3200adb 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -100,16 +100,16 @@ namespace osu.Game.Screens.Play.HUD local.DisplayOrder.Value = long.MaxValue; } - protected override void Sort() + protected override bool CheckValidScorePosition(int i) { - base.Sort(); - // change displayed position to '-' when there are 50 already submitted scores and tracked score is last if (scoreSource.Value != PlayBeatmapDetailArea.TabType.Local) { - if (TrackedScore?.ScorePosition == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) - TrackedScore.ScorePosition = null; + if (i == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) + return false; } + + return base.CheckValidScorePosition(i); } private void updateVisibility() => From 0a520c979ed9d0cae8dd8c32f83702b41c026283 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 15:59:40 +0900 Subject: [PATCH 144/152] Update test to split out per-type tests --- .../TestSceneSoloGameplayLeaderboard.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index fc1a67e285..d357c52d8a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -73,23 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value); } - [Test] - public void TestTrackedScorePosition() + [TestCase(PlayBeatmapDetailArea.TabType.Local, 51)] + [TestCase(PlayBeatmapDetailArea.TabType.Global, null)] + [TestCase(PlayBeatmapDetailArea.TabType.Country, null)] + [TestCase(PlayBeatmapDetailArea.TabType.Friends, null)] + public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex) { - AddStep("change TabType to global", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Global); - AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition! == 50); - - AddStep("change TabType to local", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Local); - AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition! == 50); + AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType); + AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) })); - AddUntilStep("tracked player is #51", () => leaderboard.TrackedScore?.ScorePosition! == 51); - - AddStep("change TabType to global", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Global); - AddUntilStep("tracked player is -", () => leaderboard.TrackedScore?.ScorePosition! == null); - - AddStep("change TabType to country", () => beatmapTabType.Value = PlayBeatmapDetailArea.TabType.Country); - AddUntilStep("tracked player is -", () => leaderboard.TrackedScore?.ScorePosition! == null); + AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex)); } [Test] From d59befc9d3a5bd740f1d6a965d58a36cc9ce8d0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 16:08:27 +0900 Subject: [PATCH 145/152] Fix initial visual state of positions before `sort` runs --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index bbde247c21..0d03d8d090 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -62,17 +62,21 @@ namespace osu.Game.Screens.Play.HUD private int? scorePosition; + private bool scorePositionIsSet; + public int? ScorePosition { get => scorePosition; set { - if (value == scorePosition) + // We always want to run once, as the incoming value may be null and require a visual update to "-". + if (value == scorePosition && scorePositionIsSet) return; scorePosition = value; positionText.Text = scorePosition.HasValue ? $"#{scorePosition.Value.FormatRank()}" : "-"; + scorePositionIsSet = true; updateState(); } From 7c08cff297d3d7c6f31ce52a074397d219a0832b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 16:10:32 +0900 Subject: [PATCH 146/152] Make new test more resilient to false passes --- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index d357c52d8a..7ad4ec2f85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -83,7 +83,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) })); - AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex)); + + AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType().First().ScorePosition != null); + + if (expectedOverflowIndex == null) + AddUntilStep($"tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); + else + AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex)); } [Test] From b86b45b6d25800d3117e2a01862c734cd17a1dbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Nov 2022 21:13:21 +0900 Subject: [PATCH 147/152] Fix excess string interpolation --- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index 7ad4ec2f85..881870921c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType().First().ScorePosition != null); if (expectedOverflowIndex == null) - AddUntilStep($"tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); + AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); else AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex)); } From 1be8c6bd9c4d48d62fb421d5fda37e332705302e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Nov 2022 17:19:32 +0300 Subject: [PATCH 148/152] Fix "perform from screen" not considering screen load state --- osu.Game/PerformFromMenuRunner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index eae13fe5d0..158350b43d 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -89,6 +89,10 @@ namespace osu.Game // check if we are already at a valid target screen. if (validScreens.Any(t => t.IsAssignableFrom(type))) { + if (!((Drawable)current).IsLoaded) + // wait until screen is loaded before invoking action. + return true; + finalAction(current); Cancel(); return true; From 8943819ee70d5e69e7b4c26b712af06067a3b206 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Nov 2022 17:49:03 +0300 Subject: [PATCH 149/152] Add test coverage --- .../Navigation/TestScenePerformFromScreen.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index d1b1ce5c4b..ce0543875b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -4,6 +4,7 @@ #nullable disable using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -85,6 +86,19 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("did perform", () => actionPerformed); } + [Test] + public void TestPerformEnsuresScreenIsLoaded() + { + TestLoadBlockingScreen screen = null; + + AddStep("push blocking screen", () => Game.ScreenStack.Push(screen = new TestLoadBlockingScreen())); + AddStep("perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestLoadBlockingScreen) })); + AddAssert("action not performed", () => !actionPerformed); + + AddStep("allow load", () => screen.LoadEvent.Set()); + AddUntilStep("action performed", () => actionPerformed); + } + [Test] public void TestOverlaysAlwaysClosed() { @@ -270,5 +284,16 @@ namespace osu.Game.Tests.Visual.Navigation return base.OnExiting(e); } } + + public class TestLoadBlockingScreen : OsuScreen + { + public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim(); + + [BackgroundDependencyLoader] + private void load() + { + LoadEvent.Wait(10000); + } + } } } From d587f29351f64b1a2bbabf7dd82cbdba2374b2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Nov 2022 21:02:23 +0100 Subject: [PATCH 150/152] Update tests to reflect desired behaviour --- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 981967e413..ee6c322ee3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestSharedClockState() + public void TestClockTimeTransferIsOneDirectional() { AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000)); AddStep("click test gameplay button", () => @@ -195,15 +195,15 @@ namespace osu.Game.Tests.Visual.Editing GameplayClockContainer gameplayClockContainer = null; AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType().First()); AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning); + // when the gameplay test is entered, the clock is expected to continue from where it was in the main editor... AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000); - double timeAtPlayerExit = 0; AddWaitStep("wait some", 5); - AddStep("store time before exit", () => timeAtPlayerExit = gameplayClockContainer.CurrentTime); AddStep("exit player", () => editorPlayer.Exit()); AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); - AddAssert("time is past player exit", () => EditorClock.CurrentTime >= timeAtPlayerExit); + // but when exiting from gameplay test back to editor, the expectation is that the editor time should revert to what it was at the point of initiating the gameplay test. + AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000)); } public override void TearDownSteps() From 016de2f5a03b34395eb0c71eae5660f42566952c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Nov 2022 05:07:10 +0300 Subject: [PATCH 151/152] Fix editor not always playing hitsounds with clock offsets applied --- osu.Game/Screens/Edit/EditorClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 81d82130da..f83874e4a0 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -270,7 +270,7 @@ namespace osu.Game.Screens.Edit { IsSeeking &= Transforms.Any(); - if (track.Value?.IsRunning != true) + if (!IsRunning) { // seeking in the editor can happen while the track isn't running. // in this case we always want to expose ourselves as seeking (to avoid sample playback). From ca67689a36339a8be554e39d017367873e244017 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Nov 2022 07:44:28 +0300 Subject: [PATCH 152/152] Fix intermittent present beatmap test failures --- .../Navigation/TestScenePresentBeatmap.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 6469962b08..02b348b439 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -7,12 +7,14 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Navigation { @@ -55,6 +57,7 @@ namespace osu.Game.Tests.Visual.Navigation presentAndConfirm(firstImport); var secondImport = importBeatmap(3); + confirmBeatmapInSongSelect(secondImport); presentAndConfirm(secondImport); // Test presenting same beatmap more than once @@ -74,6 +77,7 @@ namespace osu.Game.Tests.Visual.Navigation presentAndConfirm(firstImport); var secondImport = importBeatmap(3, new ManiaRuleset().RulesetInfo); + confirmBeatmapInSongSelect(secondImport); presentAndConfirm(secondImport); presentSecondDifficultyAndConfirm(firstImport, 1); @@ -134,13 +138,22 @@ namespace osu.Game.Tests.Visual.Navigation return () => imported; } + private void confirmBeatmapInSongSelect(Func getImport) + { + AddUntilStep("beatmap in song select", () => + { + var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen; + return songSelect.ChildrenOfType().Single().BeatmapSets.Any(b => b.MatchesOnlineID(getImport())); + }); + } + private void presentAndConfirm(Func getImport) { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); - AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.MatchesOnlineID(getImport())); - AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Beatmaps.First().Ruleset)); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded); + AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID)); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset)); } private void presentSecondDifficultyAndConfirm(Func getImport, int importedID) @@ -148,9 +161,9 @@ namespace osu.Game.Tests.Visual.Navigation Predicate pred = b => b.OnlineID == importedID * 2048; AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred)); - AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); - AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID == importedID * 2048); - AddAssert("correct ruleset selected", () => Game.Ruleset.Value.Equals(getImport().Beatmaps.First().Ruleset)); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded); + AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 2048)); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset)); } } }