From 65d664425bdfeca60343863d1ef441a7ade7a389 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 12 Apr 2019 23:13:13 +0200 Subject: [PATCH 001/189] Added DiscordRichPresence to osu.Desktop packages --- osu.Desktop/osu.Desktop.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 66db439c82..7a19e73be6 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -22,6 +22,7 @@ + From 533afaa770be793be46f9948b7168e469f33431f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 13 Apr 2019 13:06:53 +0200 Subject: [PATCH 002/189] Added DiscordRichPresenceClient --- osu.Desktop/DiscordRichPresenceClient.cs | 106 +++++++++++++++++++++++ osu.Desktop/OsuGameDesktop.cs | 2 + 2 files changed, 108 insertions(+) create mode 100644 osu.Desktop/DiscordRichPresenceClient.cs diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs new file mode 100644 index 0000000000..03b2884ab6 --- /dev/null +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using DiscordRPC; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Users; +using User = osu.Game.Users.User; + +namespace osu.Desktop +{ + internal class DiscordRichPresenceClient : Component + { + private const string client_id = "559391129716391967"; + + private Bindable user; + + private readonly DiscordRpcClient client = new DiscordRpcClient(client_id); + + [BackgroundDependencyLoader] + private void load(IAPIProvider provider) + { + user = provider.LocalUser.GetBoundCopy(); + + user.ValueChanged += usr => + { + usr.OldValue.Status.ValueChanged -= updateStatus; + usr.NewValue.Status.ValueChanged += updateStatus; + }; + + user.Value.Status.ValueChanged += updateStatus; + + client.OnReady += (_, __) => Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client : {e.Message}", LoggingTarget.Network, LogLevel.Debug); + client.OnConnectionFailed += (_, e) => Logger.Log("Discord RPC Client failed to initialize : is discord running ?", LoggingTarget.Network, LogLevel.Debug); + client.OnPresenceUpdate += (_, __) => Logger.Log("Updated Discord Rich Presence", LoggingTarget.Network, LogLevel.Debug); + + client.Initialize(); + } + + private void updateStatus(ValueChangedEvent e) + { + var presence = defaultPresence(e.NewValue.Message); + + switch (e.NewValue) + { + case UserStatusSoloGame game: + presence.State = $"{game.Beatmap.Metadata.Artist} - {game.Beatmap.Metadata.Title} [{game.Beatmap.Version}]"; + setPresenceGamemode(game.Beatmap.Ruleset, presence); + break; + + case UserStatusEditing editing: + presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title}" + (editing.Beatmap.Version != null ? $"[{editing.Beatmap.Version}]" : ""); + break; + } + + client.SetPresence(presence); + } + + private void setPresenceGamemode(RulesetInfo ruleset, RichPresence presence) + { + switch (ruleset.ID) + { + case 0: + presence.Assets.SmallImageKey = "osu"; + break; + + case 1: + presence.Assets.SmallImageKey = "taiko"; + break; + + case 2: + presence.Assets.SmallImageKey = "fruits"; + break; + + case 3: + presence.Assets.SmallImageKey = "mania"; + break; + } + + presence.Assets.SmallImageText = ruleset.ShortName; + } + + private RichPresence defaultPresence(string status) => new RichPresence + { + Details = status, + Assets = new Assets + { + LargeImageKey = "lazer", + LargeImageText = "osu!lazer" + } + }; + + protected override void Update() + { + if (client.IsInitialized) + client?.Invoke(); + + base.Update(); + } + } +} diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index e7e0af7eea..41e2efd011 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -64,6 +64,8 @@ namespace osu.Desktop else Add(new SimpleUpdateManager()); } + + Add(new DiscordRichPresenceClient()); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) From 4996ad4b042c73de5919818c2a047f19b701c75b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 13 Apr 2019 13:25:52 +0200 Subject: [PATCH 003/189] Show the current gamemode on the rich presence --- osu.Desktop/DiscordRichPresenceClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 03b2884ab6..1e7c1f051a 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -50,7 +50,7 @@ namespace osu.Desktop { case UserStatusSoloGame game: presence.State = $"{game.Beatmap.Metadata.Artist} - {game.Beatmap.Metadata.Title} [{game.Beatmap.Version}]"; - setPresenceGamemode(game.Beatmap.Ruleset, presence); + setPresenceGamemode(game.Ruleset, presence); break; case UserStatusEditing editing: From 1b1ebb7fd928f0651e7c2777ce0807e47961ea54 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 14 Apr 2019 21:12:10 +0200 Subject: [PATCH 004/189] Show current logged-in user on rich presence --- osu.Desktop/DiscordRichPresenceClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 1e7c1f051a..1f4d91dbdb 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -91,7 +91,7 @@ namespace osu.Desktop Assets = new Assets { LargeImageKey = "lazer", - LargeImageText = "osu!lazer" + LargeImageText = user.Value.Username } }; From b38160177af31069b0c5377a7d1c8eaa92fe945e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 14 Apr 2019 22:18:57 +0200 Subject: [PATCH 005/189] Add some more icons to rich presence + prevent rich presence from displaying empty brackets when current beatmap doesn't have a version (trying to edit osu! main menu theme music ?) --- osu.Desktop/DiscordRichPresenceClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 1f4d91dbdb..93c2a8bdcf 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -15,7 +15,7 @@ namespace osu.Desktop { internal class DiscordRichPresenceClient : Component { - private const string client_id = "559391129716391967"; + private const string client_id = "563024054391537674"; private Bindable user; @@ -54,7 +54,9 @@ namespace osu.Desktop break; case UserStatusEditing editing: - presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title}" + (editing.Beatmap.Version != null ? $"[{editing.Beatmap.Version}]" : ""); + presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title} " + (!string.IsNullOrEmpty(editing.Beatmap.Version) ? $"[{editing.Beatmap.Version}]" : ""); + presence.Assets.SmallImageKey = "edit"; + presence.Assets.SmallImageText = "editing"; break; } @@ -80,6 +82,10 @@ namespace osu.Desktop case 3: presence.Assets.SmallImageKey = "mania"; break; + + default: + presence.Assets.SmallImageKey = "unknown"; + break; } presence.Assets.SmallImageText = ruleset.ShortName; From a4166ce1e3991ba23a7bac15bf10ca860dace0f1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 19 Apr 2019 19:50:13 +0200 Subject: [PATCH 006/189] Removed Update() override from DiscordRichPresenceClient because client.Invoke() is unecessary (events callbacks are automatically called) --- osu.Desktop/DiscordRichPresenceClient.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 93c2a8bdcf..9288141e5f 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -100,13 +100,5 @@ namespace osu.Desktop LargeImageText = user.Value.Username } }; - - protected override void Update() - { - if (client.IsInitialized) - client?.Invoke(); - - base.Update(); - } } } From ee2bbf950f682c82e6cb31ae20f871b1958724af Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 14 May 2019 19:13:21 +0200 Subject: [PATCH 007/189] Update DiscordRichPresenceClient presence logic --- osu.Desktop/DiscordRichPresenceClient.cs | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 9288141e5f..9537252216 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -9,6 +9,7 @@ using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Users; +using static osu.Game.Users.UserActivity; using User = osu.Game.Users.User; namespace osu.Desktop @@ -28,32 +29,43 @@ namespace osu.Desktop user.ValueChanged += usr => { - usr.OldValue.Status.ValueChanged -= updateStatus; - usr.NewValue.Status.ValueChanged += updateStatus; + usr.NewValue.Activity.ValueChanged += activity => updateStatus(user.Value.Status.Value, activity.NewValue); + usr.NewValue.Status.ValueChanged += status => updateStatus(status.NewValue, user.Value.Activity.Value); }; - user.Value.Status.ValueChanged += updateStatus; + user.TriggerChange(); - client.OnReady += (_, __) => Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client : {e.Message}", LoggingTarget.Network, LogLevel.Debug); - client.OnConnectionFailed += (_, e) => Logger.Log("Discord RPC Client failed to initialize : is discord running ?", LoggingTarget.Network, LogLevel.Debug); - client.OnPresenceUpdate += (_, __) => Logger.Log("Updated Discord Rich Presence", LoggingTarget.Network, LogLevel.Debug); + enableLogging(); client.Initialize(); } - private void updateStatus(ValueChangedEvent e) + private void enableLogging() { - var presence = defaultPresence(e.NewValue.Message); + client.OnReady += (_, __) => Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client : {e.Message}", LoggingTarget.Network, LogLevel.Debug); + client.OnConnectionFailed += (_, e) => Logger.Log("Discord RPC Client failed to initialize : is discord running ?", LoggingTarget.Network, LogLevel.Debug); + client.OnPresenceUpdate += (_, __) => Logger.Log("Updated Discord Rich Presence", LoggingTarget.Network, LogLevel.Debug); + } - switch (e.NewValue) + private void updateStatus(UserStatus st, UserActivity a) + { + var presence = defaultPresence(st is UserStatusOnline ? a?.Status : st.Message); + + if (!(st is UserStatusOnline)) //don't update the presence any further if the current user status is DND / Offline & simply return with the default presence { - case UserStatusSoloGame game: + client.SetPresence(presence); + return; + } + + switch (a) + { + case UserActivitySoloGame game: presence.State = $"{game.Beatmap.Metadata.Artist} - {game.Beatmap.Metadata.Title} [{game.Beatmap.Version}]"; setPresenceGamemode(game.Ruleset, presence); break; - case UserStatusEditing editing: + case UserActivityEditing editing: presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title} " + (!string.IsNullOrEmpty(editing.Beatmap.Version) ? $"[{editing.Beatmap.Version}]" : ""); presence.Assets.SmallImageKey = "edit"; presence.Assets.SmallImageText = "editing"; From d4013ae0d894134c2397a84f2bf0427bfa026ec0 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 14 May 2019 19:37:43 +0200 Subject: [PATCH 008/189] Make DiscordRichPresenceClient dispose client on disposal --- osu.Desktop/DiscordRichPresenceClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 9537252216..d49b08823c 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -112,5 +112,11 @@ namespace osu.Desktop LargeImageText = user.Value.Username } }; + + protected override void Dispose(bool isDisposing) + { + client.Dispose(); + base.Dispose(isDisposing); + } } } From 01b75db21a791732b1e2c57951eaae7a8c2523ac Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 16 May 2019 18:36:54 +0200 Subject: [PATCH 009/189] Use ruleset.ShortName instead of hardcoded names. --- osu.Desktop/DiscordRichPresenceClient.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index d49b08823c..38e929c1ae 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -80,19 +80,19 @@ namespace osu.Desktop switch (ruleset.ID) { case 0: - presence.Assets.SmallImageKey = "osu"; + presence.Assets.SmallImageKey = ruleset.ShortName; break; case 1: - presence.Assets.SmallImageKey = "taiko"; + presence.Assets.SmallImageKey = ruleset.ShortName; break; case 2: - presence.Assets.SmallImageKey = "fruits"; + presence.Assets.SmallImageKey = ruleset.ShortName; break; case 3: - presence.Assets.SmallImageKey = "mania"; + presence.Assets.SmallImageKey = ruleset.ShortName; break; default: From 446210b81223a7635496071a1204ae64d95d25aa Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 25 May 2019 11:33:31 +0200 Subject: [PATCH 010/189] Use an if statement instead of a switch --- osu.Desktop/DiscordRichPresenceClient.cs | 26 ++++-------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 38e929c1ae..4a2f882e2b 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -77,28 +77,10 @@ namespace osu.Desktop private void setPresenceGamemode(RulesetInfo ruleset, RichPresence presence) { - switch (ruleset.ID) - { - case 0: - presence.Assets.SmallImageKey = ruleset.ShortName; - break; - - case 1: - presence.Assets.SmallImageKey = ruleset.ShortName; - break; - - case 2: - presence.Assets.SmallImageKey = ruleset.ShortName; - break; - - case 3: - presence.Assets.SmallImageKey = ruleset.ShortName; - break; - - default: - presence.Assets.SmallImageKey = "unknown"; - break; - } + if (ruleset.ID != null && ruleset.ID <= 3) //legacy rulesets use an ID between 0 and 3 + presence.Assets.SmallImageKey = ruleset.ShortName; + else + presence.Assets.SmallImageKey = "unknown"; //not a legay ruleset so let's display the unknown ruleset icon. presence.Assets.SmallImageText = ruleset.ShortName; } From 9faba94978de1db4008a2124c90f909117e4e93a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 12 Jun 2019 12:22:52 +0200 Subject: [PATCH 011/189] Fix references to UserActivities --- osu.Desktop/DiscordRichPresenceClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 4a2f882e2b..15d5770041 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -60,12 +60,12 @@ namespace osu.Desktop switch (a) { - case UserActivitySoloGame game: + case UserActivity.SoloGame game: presence.State = $"{game.Beatmap.Metadata.Artist} - {game.Beatmap.Metadata.Title} [{game.Beatmap.Version}]"; setPresenceGamemode(game.Ruleset, presence); break; - case UserActivityEditing editing: + case UserActivity.Editing editing: presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title} " + (!string.IsNullOrEmpty(editing.Beatmap.Version) ? $"[{editing.Beatmap.Version}]" : ""); presence.Assets.SmallImageKey = "edit"; presence.Assets.SmallImageText = "editing"; From 4275f70cf453aeb3799d934fe9bfd67c81040b8f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 12 Jun 2019 13:14:01 +0200 Subject: [PATCH 012/189] Fix CI inspections. --- osu.Desktop/DiscordRichPresenceClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 15d5770041..81ab995b41 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -60,12 +60,12 @@ namespace osu.Desktop switch (a) { - case UserActivity.SoloGame game: + case SoloGame game: presence.State = $"{game.Beatmap.Metadata.Artist} - {game.Beatmap.Metadata.Title} [{game.Beatmap.Version}]"; setPresenceGamemode(game.Ruleset, presence); break; - case UserActivity.Editing editing: + case Editing editing: presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title} " + (!string.IsNullOrEmpty(editing.Beatmap.Version) ? $"[{editing.Beatmap.Version}]" : ""); presence.Assets.SmallImageKey = "edit"; presence.Assets.SmallImageText = "editing"; From 54e6e4701970d2372008869d55d3f5263df0b34d Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 21 Jun 2019 12:48:13 +0200 Subject: [PATCH 013/189] Display current user activity on rich presence if current activity != null & user online status == online. --- osu.Desktop/DiscordRichPresenceClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index 81ab995b41..af5b42b275 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -50,7 +50,7 @@ namespace osu.Desktop private void updateStatus(UserStatus st, UserActivity a) { - var presence = defaultPresence(st is UserStatusOnline ? a?.Status : st.Message); + var presence = defaultPresence(st is UserStatusOnline ? a?.Status ?? st.Message : st.Message); //display the current user activity if the user status is online & user activity != null, else display the current user online status if (!(st is UserStatusOnline)) //don't update the presence any further if the current user status is DND / Offline & simply return with the default presence { @@ -80,7 +80,7 @@ namespace osu.Desktop if (ruleset.ID != null && ruleset.ID <= 3) //legacy rulesets use an ID between 0 and 3 presence.Assets.SmallImageKey = ruleset.ShortName; else - presence.Assets.SmallImageKey = "unknown"; //not a legay ruleset so let's display the unknown ruleset icon. + presence.Assets.SmallImageKey = "unknown"; //not a legacy ruleset so let's display the unknown ruleset icon. presence.Assets.SmallImageText = ruleset.ShortName; } From 8d30c35104378baee846b75c338313291543c724 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Aug 2019 03:10:58 +0300 Subject: [PATCH 014/189] Implement sorting --- osu.Game/Overlays/SocialOverlay.cs | 65 +++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 4def249200..dc63b0ab9a 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -16,6 +16,7 @@ using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.Social; using osu.Game.Users; using osu.Framework.Threading; +using System; namespace osu.Game.Overlays { @@ -71,7 +72,7 @@ namespace osu.Game.Overlays Filter.Tabs.Current.ValueChanged += _ => queueUpdate(); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); - Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdate(); + Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => onDropdownChanged(); currentQuery.BindTo(Filter.Search.Current); currentQuery.ValueChanged += query => @@ -175,11 +176,71 @@ namespace osu.Game.Overlays private void updateUsers(IEnumerable newUsers) { - Users = newUsers; + var sortDirection = Filter.DisplayStyleControl.Dropdown.Current.Value; + + IEnumerable sortedUsers = newUsers; + + switch (Filter.Tabs.Current.Value) + { + case SocialSortCriteria.Location: + switch (sortDirection) + { + case SortDirection.Ascending: + sortedUsers = newUsers.OrderBy(u => u.Country.FullName); + break; + + case SortDirection.Descending: + sortedUsers = newUsers.OrderByDescending(u => u.Country.FullName); + break; + } + break; + + case SocialSortCriteria.Name: + switch (sortDirection) + { + case SortDirection.Ascending: + sortedUsers = newUsers.OrderBy(u => u.Username); + break; + + case SortDirection.Descending: + sortedUsers = newUsers.OrderByDescending(u => u.Username); + break; + } + break; + + case SocialSortCriteria.Rank: + if (newUsers.FirstOrDefault().Statistics != null) + { + switch (sortDirection) + { + case SortDirection.Ascending: + sortedUsers = newUsers.OrderBy(u => u.Statistics?.Ranks.Global); + break; + + case SortDirection.Descending: + sortedUsers = newUsers.OrderByDescending(u => u.Statistics?.Ranks.Global); + break; + } + } + break; + } + + Users = sortedUsers; loading.Hide(); recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } + private void onDropdownChanged() + { + if (Users == null) + { + queueUpdate(); + return; + } + + updateUsers(Users); + } + private void clearPanels() { if (panels != null) From cb81d1dd2fea0a174777044e7fdd8fa451f977f6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Aug 2019 03:53:16 +0300 Subject: [PATCH 015/189] Better use of loading animation --- osu.Game/Overlays/SocialOverlay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index dc63b0ab9a..2202c473f6 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -103,7 +103,6 @@ namespace osu.Game.Overlays Users = null; clearPanels(); - loading.Hide(); getUsersRequest?.Cancel(); if (API?.IsLoggedIn != true) @@ -131,8 +130,13 @@ namespace osu.Game.Overlays { clearPanels(); + loading.Show(); + if (Users == null) + { + loading.Hide(); return; + } var newPanels = new FillFlowContainer { @@ -167,15 +171,15 @@ namespace osu.Game.Overlays LoadComponentAsync(newPanels, f => { - if (panels != null) - ScrollFlow.Remove(panels); - + loading.Hide(); ScrollFlow.Add(panels = newPanels); }); } private void updateUsers(IEnumerable newUsers) { + loading.Show(); + var sortDirection = Filter.DisplayStyleControl.Dropdown.Current.Value; IEnumerable sortedUsers = newUsers; @@ -226,7 +230,6 @@ namespace osu.Game.Overlays } Users = sortedUsers; - loading.Hide(); recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } From 7cc6494482c7ba413012c5aa527da25b1c66eb41 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Aug 2019 03:54:49 +0300 Subject: [PATCH 016/189] Remove sorting by rank Since it isn't working for any case currently --- osu.Game/Overlays/SocialOverlay.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 2202c473f6..8353e2d683 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -211,22 +211,6 @@ namespace osu.Game.Overlays break; } break; - - case SocialSortCriteria.Rank: - if (newUsers.FirstOrDefault().Statistics != null) - { - switch (sortDirection) - { - case SortDirection.Ascending: - sortedUsers = newUsers.OrderBy(u => u.Statistics?.Ranks.Global); - break; - - case SortDirection.Descending: - sortedUsers = newUsers.OrderByDescending(u => u.Statistics?.Ranks.Global); - break; - } - } - break; } Users = sortedUsers; From 66b27875e06dedb156b76caacdedc213d6affb87 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Aug 2019 04:00:22 +0300 Subject: [PATCH 017/189] Fix possible null exception --- osu.Game/Overlays/SocialOverlay.cs | 49 ++++++++++++++++-------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 8353e2d683..22d1471229 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -184,33 +184,36 @@ namespace osu.Game.Overlays IEnumerable sortedUsers = newUsers; - switch (Filter.Tabs.Current.Value) + if (sortedUsers.Any()) { - case SocialSortCriteria.Location: - switch (sortDirection) - { - case SortDirection.Ascending: - sortedUsers = newUsers.OrderBy(u => u.Country.FullName); - break; + switch (Filter.Tabs.Current.Value) + { + case SocialSortCriteria.Location: + switch (sortDirection) + { + case SortDirection.Ascending: + sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName); + break; - case SortDirection.Descending: - sortedUsers = newUsers.OrderByDescending(u => u.Country.FullName); - break; - } - break; + case SortDirection.Descending: + sortedUsers = sortedUsers.OrderByDescending(u => u.Country.FullName); + break; + } + break; - case SocialSortCriteria.Name: - switch (sortDirection) - { - case SortDirection.Ascending: - sortedUsers = newUsers.OrderBy(u => u.Username); - break; + case SocialSortCriteria.Name: + switch (sortDirection) + { + case SortDirection.Ascending: + sortedUsers = sortedUsers.OrderBy(u => u.Username); + break; - case SortDirection.Descending: - sortedUsers = newUsers.OrderByDescending(u => u.Username); - break; - } - break; + case SortDirection.Descending: + sortedUsers = sortedUsers.OrderByDescending(u => u.Username); + break; + } + break; + } } Users = sortedUsers; From 385bc6f52988c3f8c9858b8525aeceb11e6b33fa Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Aug 2019 04:11:22 +0300 Subject: [PATCH 018/189] Remove using and add blank lines --- osu.Game/Overlays/SocialOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 22d1471229..23185ed989 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -16,7 +16,6 @@ using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.Social; using osu.Game.Users; using osu.Framework.Threading; -using System; namespace osu.Game.Overlays { @@ -199,6 +198,7 @@ namespace osu.Game.Overlays sortedUsers = sortedUsers.OrderByDescending(u => u.Country.FullName); break; } + break; case SocialSortCriteria.Name: @@ -212,6 +212,7 @@ namespace osu.Game.Overlays sortedUsers = sortedUsers.OrderByDescending(u => u.Username); break; } + break; } } From 9223a1ba8a4ff25c82e719374e37a3f3f3a90f81 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Aug 2019 05:03:47 +0300 Subject: [PATCH 019/189] Simplify sorting logic --- osu.Game/Overlays/SocialOverlay.cs | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 23185ed989..e2179361b1 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -16,6 +16,7 @@ using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.Social; using osu.Game.Users; using osu.Framework.Threading; +using System; namespace osu.Game.Overlays { @@ -188,31 +189,11 @@ namespace osu.Game.Overlays switch (Filter.Tabs.Current.Value) { case SocialSortCriteria.Location: - switch (sortDirection) - { - case SortDirection.Ascending: - sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName); - break; - - case SortDirection.Descending: - sortedUsers = sortedUsers.OrderByDescending(u => u.Country.FullName); - break; - } - + sortedUsers = sortBy(sortedUsers, u => u.Country.FullName, sortDirection); break; case SocialSortCriteria.Name: - switch (sortDirection) - { - case SortDirection.Ascending: - sortedUsers = sortedUsers.OrderBy(u => u.Username); - break; - - case SortDirection.Descending: - sortedUsers = sortedUsers.OrderByDescending(u => u.Username); - break; - } - + sortedUsers = sortBy(sortedUsers, u => u.Username, sortDirection); break; } } @@ -221,6 +202,9 @@ namespace osu.Game.Overlays recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } + private IEnumerable sortBy(IEnumerable users, Func condition, SortDirection sortDirection) => + sortDirection == SortDirection.Ascending ? users.OrderBy(condition) : users.OrderByDescending(condition); + private void onDropdownChanged() { if (Users == null) From 11df8c5576478f920f3c09e3b8c884623a0cb836 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Aug 2019 01:02:26 +0300 Subject: [PATCH 020/189] Remove a lot of loading animation calls --- osu.Game/Overlays/SocialOverlay.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index e2179361b1..a467ae6dfa 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -122,16 +122,12 @@ namespace osu.Game.Overlays API.Queue(getUsersRequest = userRequest); break; } - - loading.Show(); } private void recreatePanels(PanelDisplayStyle displayStyle) { clearPanels(); - loading.Show(); - if (Users == null) { loading.Hide(); @@ -178,8 +174,6 @@ namespace osu.Game.Overlays private void updateUsers(IEnumerable newUsers) { - loading.Show(); - var sortDirection = Filter.DisplayStyleControl.Dropdown.Current.Value; IEnumerable sortedUsers = newUsers; @@ -218,6 +212,8 @@ namespace osu.Game.Overlays private void clearPanels() { + loading.Show(); + if (panels != null) { panels.Expire(); From 9b1e8cf48b701aad40f4a3b34a4d14205bc5bb77 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Aug 2019 01:03:51 +0300 Subject: [PATCH 021/189] Use CancelDelayedTasks instead of private ScheduledDelegate --- osu.Game/Overlays/SocialOverlay.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index a467ae6dfa..5ed6a9a703 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -77,12 +77,12 @@ namespace osu.Game.Overlays currentQuery.BindTo(Filter.Search.Current); currentQuery.ValueChanged += query => { - queryChangedDebounce?.Cancel(); + Scheduler.CancelDelayedTasks(); if (string.IsNullOrEmpty(query.NewValue)) queueUpdate(); else - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); + Scheduler.AddDelayed(updateSearch, 500); }; } @@ -90,13 +90,11 @@ namespace osu.Game.Overlays private readonly Bindable currentQuery = new Bindable(); - private ScheduledDelegate queryChangedDebounce; - private void queueUpdate() => Scheduler.AddOnce(updateSearch); private void updateSearch() { - queryChangedDebounce?.Cancel(); + Scheduler.CancelDelayedTasks(); if (!IsLoaded) return; From 2d7024ffd92656a9d3646abc0e87111709e6bfe4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Aug 2019 01:14:35 +0300 Subject: [PATCH 022/189] Use CancellationTokenSource to avoid unwanted panels creation --- osu.Game/Overlays/SocialOverlay.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 5ed6a9a703..ee3adf65b9 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.Social; using osu.Game.Users; using osu.Framework.Threading; using System; +using System.Threading; namespace osu.Game.Overlays { @@ -92,6 +93,8 @@ namespace osu.Game.Overlays private void queueUpdate() => Scheduler.AddOnce(updateSearch); + private CancellationTokenSource loadCancellation; + private void updateSearch() { Scheduler.CancelDelayedTasks(); @@ -132,6 +135,8 @@ namespace osu.Game.Overlays return; } + loadCancellation = new CancellationTokenSource(); + var newPanels = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -167,7 +172,7 @@ namespace osu.Game.Overlays { loading.Hide(); ScrollFlow.Add(panels = newPanels); - }); + }, loadCancellation.Token); } private void updateUsers(IEnumerable newUsers) @@ -212,6 +217,8 @@ namespace osu.Game.Overlays { loading.Show(); + loadCancellation?.Cancel(); + if (panels != null) { panels.Expire(); From 6aef05f5d877dfd29b2035cdcadb5f36aa706e3a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Aug 2019 01:16:55 +0300 Subject: [PATCH 023/189] Remove useless function --- osu.Game/Overlays/SocialOverlay.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index ee3adf65b9..97e1c2b5a4 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays Filter.Tabs.Current.ValueChanged += _ => queueUpdate(); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); - Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => onDropdownChanged(); + Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => updateUsers(Users); currentQuery.BindTo(Filter.Search.Current); currentQuery.ValueChanged += query => @@ -202,17 +202,6 @@ namespace osu.Game.Overlays private IEnumerable sortBy(IEnumerable users, Func condition, SortDirection sortDirection) => sortDirection == SortDirection.Ascending ? users.OrderBy(condition) : users.OrderByDescending(condition); - private void onDropdownChanged() - { - if (Users == null) - { - queueUpdate(); - return; - } - - updateUsers(Users); - } - private void clearPanels() { loading.Show(); From b6b4173a8481987f3afc73f90d3cc712e099f000 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Aug 2019 01:27:16 +0300 Subject: [PATCH 024/189] Remove unused using --- osu.Game/Overlays/SocialOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 97e1c2b5a4..bf306f9569 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -15,7 +15,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.Social; using osu.Game.Users; -using osu.Framework.Threading; using System; using System.Threading; From 3227dc87fbac1c47fb505c4e66a66ac80a935678 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 6 Sep 2019 22:56:46 +0300 Subject: [PATCH 025/189] Don't use CancelDelayedTasks to avoid cancelling unwanted tasks --- osu.Game/Overlays/SocialOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index bf306f9569..1912a2c3e0 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.Social; using osu.Game.Users; using System; using System.Threading; +using osu.Framework.Threading; namespace osu.Game.Overlays { @@ -77,12 +78,12 @@ namespace osu.Game.Overlays currentQuery.BindTo(Filter.Search.Current); currentQuery.ValueChanged += query => { - Scheduler.CancelDelayedTasks(); + queryChangedDebounce?.Cancel(); if (string.IsNullOrEmpty(query.NewValue)) queueUpdate(); else - Scheduler.AddDelayed(updateSearch, 500); + queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); }; } @@ -90,13 +91,15 @@ namespace osu.Game.Overlays private readonly Bindable currentQuery = new Bindable(); + private ScheduledDelegate queryChangedDebounce; + private void queueUpdate() => Scheduler.AddOnce(updateSearch); private CancellationTokenSource loadCancellation; private void updateSearch() { - Scheduler.CancelDelayedTasks(); + queryChangedDebounce?.Cancel(); if (!IsLoaded) return; @@ -169,6 +172,9 @@ namespace osu.Game.Overlays LoadComponentAsync(newPanels, f => { + if (panels != null) + ScrollFlow.Remove(panels); + loading.Hide(); ScrollFlow.Add(panels = newPanels); }, loadCancellation.Token); From 44412f9ddb0407318c7b9603484f806ef717266e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 19 Sep 2019 03:35:56 +0300 Subject: [PATCH 026/189] Fix local sorting calls an online request --- osu.Game/Overlays/SocialOverlay.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 1912a2c3e0..7fd9ef153e 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays Header.Tabs.Current.ValueChanged += _ => queueUpdate(); - Filter.Tabs.Current.ValueChanged += _ => queueUpdate(); + Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate(); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => updateUsers(Users); @@ -180,6 +180,17 @@ namespace osu.Game.Overlays }, loadCancellation.Token); } + private void onFilterUpdate() + { + if (Users == null || Filter.Tabs.Current.Value == SocialSortCriteria.Rank) + { + queueUpdate(); + return; + } + + updateUsers(Users); + } + private void updateUsers(IEnumerable newUsers) { var sortDirection = Filter.DisplayStyleControl.Dropdown.Current.Value; From 647433a8d1c21187d86f656571d0abe8b5a0423f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 19 Sep 2019 04:09:14 +0300 Subject: [PATCH 027/189] Don't trigger request if there are no avaliable users --- osu.Game/Overlays/SocialOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 7fd9ef153e..a77172c90c 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -182,7 +182,7 @@ namespace osu.Game.Overlays private void onFilterUpdate() { - if (Users == null || Filter.Tabs.Current.Value == SocialSortCriteria.Rank) + if (Filter.Tabs.Current.Value == SocialSortCriteria.Rank) { queueUpdate(); return; From 20edaf4ba6a04adf9836ef4c3f2b2b61991e6a21 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 23 Nov 2019 17:32:16 +0000 Subject: [PATCH 028/189] add cinema mod support --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 6 ++++-- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 6 ++++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 6 ++++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 6 ++++-- osu.Game/Rulesets/Mods/ModCinema.cs | 24 ++++++++++++++++++++++-- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 71d68ace94..bf2b1c0def 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -51,7 +51,9 @@ namespace osu.Game.Rulesets.Catch else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new CatchModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Autoplay)) + if (mods.HasFlag(LegacyMods.Cinema)) + yield return new CatchModCinema(); + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new CatchModAutoplay(); if (mods.HasFlag(LegacyMods.Easy)) @@ -101,7 +103,7 @@ namespace osu.Game.Rulesets.Catch case ModType.Automation: return new Mod[] { - new MultiMod(new CatchModAutoplay(), new ModCinema()), + new MultiMod(new CatchModAutoplay(), new CatchModCinema()), new CatchModRelax(), }; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index c74a292331..c632cc0b7b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -51,7 +51,9 @@ namespace osu.Game.Rulesets.Mania else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new ManiaModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Autoplay)) + if (mods.HasFlag(LegacyMods.Cinema)) + yield return new ManiaModCinema(); + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new ManiaModAutoplay(); if (mods.HasFlag(LegacyMods.Easy)) @@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Mania case ModType.Automation: return new Mod[] { - new MultiMod(new ManiaModAutoplay(), new ModCinema()), + new MultiMod(new ManiaModAutoplay(), new ManiaModCinema()), }; case ModType.Fun: diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index fa69cec78d..2b5a0df3ed 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -60,7 +60,9 @@ namespace osu.Game.Rulesets.Osu if (mods.HasFlag(LegacyMods.Autopilot)) yield return new OsuModAutopilot(); - if (mods.HasFlag(LegacyMods.Autoplay)) + if (mods.HasFlag(LegacyMods.Cinema)) + yield return new OsuModCinema(); + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new OsuModAutoplay(); if (mods.HasFlag(LegacyMods.Easy)) @@ -126,7 +128,7 @@ namespace osu.Game.Rulesets.Osu case ModType.Automation: return new Mod[] { - new MultiMod(new OsuModAutoplay(), new ModCinema()), + new MultiMod(new OsuModAutoplay(), new OsuModCinema()), new OsuModRelax(), new OsuModAutopilot(), }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index b2655f592c..3f3a198f4a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -50,7 +50,9 @@ namespace osu.Game.Rulesets.Taiko else if (mods.HasFlag(LegacyMods.SuddenDeath)) yield return new TaikoModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Autoplay)) + if (mods.HasFlag(LegacyMods.Cinema)) + yield return new TaikoModCinema(); + else if (mods.HasFlag(LegacyMods.Autoplay)) yield return new TaikoModAutoplay(); if (mods.HasFlag(LegacyMods.Easy)) @@ -100,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko case ModType.Automation: return new Mod[] { - new MultiMod(new TaikoModAutoplay(), new ModCinema()), + new MultiMod(new TaikoModAutoplay(), new TaikoModCinema()), new TaikoModRelax(), }; diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 3c6a3a54aa..4396c3384b 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -3,15 +3,35 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Mods { - public class ModCinema : ModAutoplay + public abstract class ModCinema : ModCinema, IApplicableToDrawableRuleset + where T : HitObject + { + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); + + drawableRuleset.Playfield.AlwaysPresent = true; + drawableRuleset.Playfield.Hide(); + } + } + + public class ModCinema : ModAutoplay, IApplicableToHUD { public override string Name => "Cinema"; public override string Acronym => "CN"; - public override bool HasImplementation => false; public override IconUsage Icon => OsuIcon.ModCinema; public override string Description => "Watch the video without visual distractions."; + + public void ApplyToHUD(HUDOverlay overlay) + { + overlay.AlwaysPresent = true; + overlay.Hide(); + } } } From 3b9f59cb335bb84964737fca27138668127cd371 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 23 Nov 2019 17:34:53 +0000 Subject: [PATCH 029/189] add cinema mod support --- .../Mods/CatchModCinema.cs | 21 ++++++++++++++++ .../Mods/ManiaModCinema.cs | 22 ++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 25 +++++++++++++++++++ .../Mods/TaikoModCinema.cs | 21 ++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs create mode 100644 osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs new file mode 100644 index 0000000000..3bc1ee5bf5 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.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.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModCinema : ModCinema + { + public override Score CreateReplayScore(IBeatmap beatmap) => new Score + { + ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, + Replay = new CatchAutoGenerator(beatmap).Generate(), + }; + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs new file mode 100644 index 0000000000..02c1fc1b79 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs @@ -0,0 +1,22 @@ +// 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.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModCinema : ModCinema + { + public override Score CreateReplayScore(IBeatmap beatmap) => new Score + { + ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, + Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), + }; + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs new file mode 100644 index 0000000000..5d9a524577 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModCinema : ModCinema + { + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); + + public override Score CreateReplayScore(IBeatmap beatmap) => new Score + { + ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, + Replay = new OsuAutoGenerator(beatmap).Generate() + }; + } +} diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs new file mode 100644 index 0000000000..71aa007d3b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.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.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModCinema : ModCinema + { + public override Score CreateReplayScore(IBeatmap beatmap) => new Score + { + ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, + Replay = new TaikoAutoGenerator(beatmap).Generate(), + }; + } +} From b8e5796af51eb47f8f897906b7da72b3b1cbc9d5 Mon Sep 17 00:00:00 2001 From: Albie Date: Sun, 24 Nov 2019 07:37:06 +0000 Subject: [PATCH 030/189] add forced video/storyboard and disabled dim for mod inside new interface --- .../Graphics/Containers/UserDimContainer.cs | 6 +++++ osu.Game/Rulesets/Mods/IApplicableToScreen.cs | 26 +++++++++++++++++++ osu.Game/Rulesets/Mods/ModCinema.cs | 9 ++++++- osu.Game/Screens/Play/DimmableStoryboard.cs | 4 +-- osu.Game/Screens/Play/DimmableVideo.cs | 5 ++-- osu.Game/Screens/Play/Player.cs | 20 ++++++++++++-- 6 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToScreen.cs diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 7683bbcd63..42a25a79b1 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -21,6 +21,11 @@ namespace osu.Game.Graphics.Containers /// public readonly Bindable EnableUserDim = new Bindable(true); + /// + /// Whether or not user-configured settings relating to visibility of elements should be ignored + /// + public readonly Bindable IgnoreUserSettings = new Bindable(); + /// /// Whether or not the storyboard loaded should completely hide the background behind it. /// @@ -63,6 +68,7 @@ namespace osu.Game.Graphics.Containers ShowStoryboard.ValueChanged += _ => UpdateVisuals(); ShowVideo.ValueChanged += _ => UpdateVisuals(); StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); + IgnoreUserSettings.ValueChanged += _ => UpdateVisuals(); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Mods/IApplicableToScreen.cs b/osu.Game/Rulesets/Mods/IApplicableToScreen.cs new file mode 100644 index 0000000000..f1a631ccba --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToScreen.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. + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for a mod which can temporarily override screen settings. + /// + public interface IApplicableToScreen : IApplicableMod + { + /// + /// Whether to enable image, video and storyboard dimming + /// + bool EnableDim { get; } + + /// + /// Weather to force the video (if present) + /// + bool ForceVideo { get; } + + /// + /// Weather to force the storyboard (if present) + /// + bool ForceStoryboard { get; } + } +} diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 4396c3384b..8a777b364a 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -1,7 +1,10 @@ // 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.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; @@ -21,7 +24,7 @@ namespace osu.Game.Rulesets.Mods } } - public class ModCinema : ModAutoplay, IApplicableToHUD + public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToScreen { public override string Name => "Cinema"; public override string Acronym => "CN"; @@ -33,5 +36,9 @@ namespace osu.Game.Rulesets.Mods overlay.AlwaysPresent = true; overlay.Hide(); } + + public bool EnableDim => false; + public bool ForceVideo => true; + public bool ForceStoryboard => true; } } diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 2154526e54..4c7bb272cc 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -33,14 +33,14 @@ namespace osu.Game.Screens.Play base.LoadComplete(); } - protected override bool ShowDimContent => ShowStoryboard.Value && DimLevel < 1; + protected override bool ShowDimContent => IgnoreUserSettings.Value || ShowStoryboard.Value && DimLevel < 1; private void initializeStoryboard(bool async) { if (drawableStoryboard != null) return; - if (!ShowStoryboard.Value) + if (!ShowStoryboard.Value && !IgnoreUserSettings.Value) return; drawableStoryboard = storyboard.CreateDrawable(); diff --git a/osu.Game/Screens/Play/DimmableVideo.cs b/osu.Game/Screens/Play/DimmableVideo.cs index 4d6c10d69d..09ec5a3f5d 100644 --- a/osu.Game/Screens/Play/DimmableVideo.cs +++ b/osu.Game/Screens/Play/DimmableVideo.cs @@ -2,6 +2,7 @@ // 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; @@ -33,7 +34,7 @@ namespace osu.Game.Screens.Play base.LoadComplete(); } - protected override bool ShowDimContent => ShowVideo.Value && DimLevel < 1; + protected override bool ShowDimContent => IgnoreUserSettings.Value || ShowVideo.Value && DimLevel < 1; private void initializeVideo(bool async) { @@ -43,7 +44,7 @@ namespace osu.Game.Screens.Play if (drawableVideo != null) return; - if (!ShowVideo.Value) + if (!ShowVideo.Value && !IgnoreUserSettings.Value) return; drawableVideo = new DrawableVideo(video); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d6488dc209..3d6a9fe78f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -498,8 +498,24 @@ namespace osu.Game.Screens.Play .Delay(250) .FadeIn(250); - Background.EnableUserDim.Value = true; - Background.BlurAmount.Value = 0; + var screenOverride = Mods.Value.OfType(); + + if (screenOverride.Count() == 1) + { + var setting = screenOverride.Single(); + + Background.EnableUserDim.Value = setting.EnableDim; + DimmableVideo.EnableUserDim.Value = setting.EnableDim; + DimmableStoryboard.EnableUserDim.Value = setting.EnableDim; + + DimmableVideo.IgnoreUserSettings.Value = setting.ForceVideo; + DimmableStoryboard.IgnoreUserSettings.Value = setting.ForceStoryboard; + } + else + { + Background.EnableUserDim.Value = true; + Background.BlurAmount.Value = 0; + } Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From 1d6665fe57510c4dc950881b15728c5e64e7fa74 Mon Sep 17 00:00:00 2001 From: Albie Date: Sun, 24 Nov 2019 07:42:39 +0000 Subject: [PATCH 031/189] improve code quality using resharper and codefactor advice --- osu.Game/Rulesets/Mods/ModCinema.cs | 3 --- osu.Game/Screens/Play/DimmableStoryboard.cs | 2 +- osu.Game/Screens/Play/DimmableVideo.cs | 3 +-- osu.Game/Screens/Play/Player.cs | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 8a777b364a..5a876dbf51 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -1,10 +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.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 4c7bb272cc..0fe315fbab 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play base.LoadComplete(); } - protected override bool ShowDimContent => IgnoreUserSettings.Value || ShowStoryboard.Value && DimLevel < 1; + protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && DimLevel < 1); private void initializeStoryboard(bool async) { diff --git a/osu.Game/Screens/Play/DimmableVideo.cs b/osu.Game/Screens/Play/DimmableVideo.cs index 09ec5a3f5d..1a01cace17 100644 --- a/osu.Game/Screens/Play/DimmableVideo.cs +++ b/osu.Game/Screens/Play/DimmableVideo.cs @@ -2,7 +2,6 @@ // 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; @@ -34,7 +33,7 @@ namespace osu.Game.Screens.Play base.LoadComplete(); } - protected override bool ShowDimContent => IgnoreUserSettings.Value || ShowVideo.Value && DimLevel < 1; + protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowVideo.Value && DimLevel < 1); private void initializeVideo(bool async) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3d6a9fe78f..dc4612a525 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -432,7 +432,7 @@ namespace osu.Game.Screens.Play // cannot pause if we are already in a fail state && !HasFailed // cannot pause if already paused (or in a cooldown state) unless we are in a resuming state. - && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive)); + && (IsResuming || GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive); private bool pauseCooldownActive => lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; From 9a8e3fe1da79ea53328b2f293b7996eaa43bb138 Mon Sep 17 00:00:00 2001 From: Albie Date: Sun, 24 Nov 2019 07:44:35 +0000 Subject: [PATCH 032/189] add brackets --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc4612a525..3d6a9fe78f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -432,7 +432,7 @@ namespace osu.Game.Screens.Play // cannot pause if we are already in a fail state && !HasFailed // cannot pause if already paused (or in a cooldown state) unless we are in a resuming state. - && (IsResuming || GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive); + && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive)); private bool pauseCooldownActive => lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; From 9fdbb2a58e8aea3e7232d9853170d96502c3e2bb Mon Sep 17 00:00:00 2001 From: Albie Date: Mon, 25 Nov 2019 07:24:29 +0000 Subject: [PATCH 033/189] change name of interface and expose method instead of seperate values --- osu.Game/Rulesets/Mods/IApplicableToPlayer.cs | 15 +++++++++++ osu.Game/Rulesets/Mods/IApplicableToScreen.cs | 26 ------------------ osu.Game/Rulesets/Mods/ModCinema.cs | 14 +++++++--- osu.Game/Screens/Play/Player.cs | 27 +++++-------------- .../Play/ScreenWithBeatmapBackground.cs | 2 +- 5 files changed, 33 insertions(+), 51 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToPlayer.cs delete mode 100644 osu.Game/Rulesets/Mods/IApplicableToScreen.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToPlayer.cs b/osu.Game/Rulesets/Mods/IApplicableToPlayer.cs new file mode 100644 index 0000000000..bf78428470 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToPlayer.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 osu.Game.Screens.Play; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for a mod which can temporarily override the settings. + /// + public interface IApplicableToPlayer : IApplicableMod + { + void ApplyToPlayer(Player player); + } +} diff --git a/osu.Game/Rulesets/Mods/IApplicableToScreen.cs b/osu.Game/Rulesets/Mods/IApplicableToScreen.cs deleted file mode 100644 index f1a631ccba..0000000000 --- a/osu.Game/Rulesets/Mods/IApplicableToScreen.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Mods -{ - /// - /// An interface for a mod which can temporarily override screen settings. - /// - public interface IApplicableToScreen : IApplicableMod - { - /// - /// Whether to enable image, video and storyboard dimming - /// - bool EnableDim { get; } - - /// - /// Weather to force the video (if present) - /// - bool ForceVideo { get; } - - /// - /// Weather to force the storyboard (if present) - /// - bool ForceStoryboard { get; } - } -} diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 5a876dbf51..77bf80b149 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods } } - public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToScreen + public class ModCinema : ModAutoplay, IApplicableToHUD, IApplicableToPlayer { public override string Name => "Cinema"; public override string Acronym => "CN"; @@ -34,8 +34,14 @@ namespace osu.Game.Rulesets.Mods overlay.Hide(); } - public bool EnableDim => false; - public bool ForceVideo => true; - public bool ForceStoryboard => true; + public void ApplyToPlayer(Player player) + { + player.Background.EnableUserDim.Value = false; + player.DimmableVideo.EnableUserDim.Value = false; + player.DimmableStoryboard.EnableUserDim.Value = false; + + player.DimmableVideo.IgnoreUserSettings.Value = true; + player.DimmableStoryboard.IgnoreUserSettings.Value = true; + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3d6a9fe78f..90171da747 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -80,8 +80,8 @@ namespace osu.Game.Screens.Play protected GameplayClockContainer GameplayClockContainer { get; private set; } - protected DimmableStoryboard DimmableStoryboard { get; private set; } - protected DimmableVideo DimmableVideo { get; private set; } + public DimmableStoryboard DimmableStoryboard { get; private set; } + public DimmableVideo DimmableVideo { get; private set; } [Cached] [Cached(Type = typeof(IBindable>))] @@ -498,24 +498,8 @@ namespace osu.Game.Screens.Play .Delay(250) .FadeIn(250); - var screenOverride = Mods.Value.OfType(); - - if (screenOverride.Count() == 1) - { - var setting = screenOverride.Single(); - - Background.EnableUserDim.Value = setting.EnableDim; - DimmableVideo.EnableUserDim.Value = setting.EnableDim; - DimmableStoryboard.EnableUserDim.Value = setting.EnableDim; - - DimmableVideo.IgnoreUserSettings.Value = setting.ForceVideo; - DimmableStoryboard.IgnoreUserSettings.Value = setting.ForceStoryboard; - } - else - { - Background.EnableUserDim.Value = true; - Background.BlurAmount.Value = 0; - } + Background.EnableUserDim.Value = true; + Background.BlurAmount.Value = 0; Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); @@ -525,6 +509,9 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Restart(); GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToPlayer(this); + foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); } diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs index d7d2c97598..8eb253608b 100644 --- a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs @@ -9,6 +9,6 @@ namespace osu.Game.Screens.Play { protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); - protected new BackgroundScreenBeatmap Background => (BackgroundScreenBeatmap)base.Background; + public new BackgroundScreenBeatmap Background => (BackgroundScreenBeatmap)base.Background; } } From 944835da234c0c300af3a38a4ae45052237052be Mon Sep 17 00:00:00 2001 From: Ganendra Afrasya Date: Thu, 28 Nov 2019 12:01:40 +0700 Subject: [PATCH 034/189] Add multiplier score text on mods footer button --- osu.Game/Screens/Select/FooterButtonMods.cs | 44 +++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 29b1364944..8dc0a2a132 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -7,11 +7,14 @@ using osu.Framework.Graphics.Containers; using osu.Game.Screens.Play.HUD; using osu.Game.Rulesets.Mods; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Screens.Select @@ -24,23 +27,40 @@ namespace osu.Game.Screens.Select set => modDisplay.Current = value; } + protected readonly OsuSpriteText MultiplierText; private readonly FooterModDisplay modDisplay; + private Color4 lowMultiplierColour; + private Color4 highMultiplierColour; public FooterButtonMods() { - Add(new Container + Add(new FillFlowContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, Shear = -SHEAR, - Child = modDisplay = new FooterModDisplay + Children = new Drawable[] { - DisplayUnrankedText = false, - Scale = new Vector2(0.8f) + modDisplay = new FooterModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DisplayUnrankedText = false, + Scale = new Vector2(0.8f) + }, + MultiplierText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + } }, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 70 } }); + + Current.ValueChanged += _ => updateMultiplierText(); } [BackgroundDependencyLoader] @@ -48,10 +68,26 @@ namespace osu.Game.Screens.Select { SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); + lowMultiplierColour = colours.Red; + highMultiplierColour = colours.Green; Text = @"mods"; Hotkey = Key.F1; } + private void updateMultiplierText() + { + var multiplier = Current.Value.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + + MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + if (multiplier > 1.0) + MultiplierText.FadeColour(highMultiplierColour, 200); + else if (multiplier < 1.0) + MultiplierText.FadeColour(lowMultiplierColour, 200); + else + MultiplierText.FadeColour(Color4.White, 200); + } + private class FooterModDisplay : ModDisplay { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false; From 6149482b5d81d67778ab66043926dec885802374 Mon Sep 17 00:00:00 2001 From: Ganendra Afrasya Date: Thu, 28 Nov 2019 12:03:11 +0700 Subject: [PATCH 035/189] Add test --- .../UserInterface/TestCaseFooterButtonMods.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs new file mode 100644 index 0000000000..0c437800bc --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs @@ -0,0 +1,70 @@ +// 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 NUnit.Framework; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Select; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestCaseFooterButtonMods : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(FooterButtonMods) + }; + + private readonly TestFooterButtonMods footerButtonMods; + + public TestCaseFooterButtonMods() + { + Add(footerButtonMods = new TestFooterButtonMods()); + } + + [Test] + public void TestIncrementMultiplier() + { + AddStep(@"Add Hidden", () => changeMods(new Mod[] { new OsuModHidden() })); + AddAssert(@"Check Hidden multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.06x")); + AddStep(@"Add HardRock", () => changeMods(new Mod[] { new OsuModHidden() })); + AddAssert(@"Check HardRock multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.06x")); + AddStep(@"Add DoubleTime", () => changeMods(new Mod[] { new OsuModDoubleTime() })); + AddAssert(@"Check DoubleTime multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.12x")); + AddStep(@"Add multiple Mods", () => changeMods(new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHidden() })); + AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.26x")); + } + + [Test] + public void TestDecrementMultiplier() + { + AddStep(@"Add Easy", () => changeMods(new Mod[] { new OsuModEasy() })); + AddAssert(@"Check Easy multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"0.50x")); + AddStep(@"Add NoFail", () => changeMods(new Mod[] { new OsuModNoFail() })); + AddAssert(@"Check NoFail multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"0.50x")); + AddStep(@"Add Multiple Mods", () => changeMods(new Mod[] { new OsuModEasy(), new OsuModNoFail() })); + AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"0.25x")); + } + + [Test] + public void TestClearMultiplier() + { + AddStep(@"Add mods", () => changeMods(new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() })); + AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); + AddAssert(@"Check empty multiplier", () => footerButtonMods.MultiplierText.Text.Equals(string.Empty)); + } + + private void changeMods(IReadOnlyList mods) + { + footerButtonMods.Current.Value = mods; + } + + private class TestFooterButtonMods : FooterButtonMods + { + public new OsuSpriteText MultiplierText => base.MultiplierText; + } + } +} From ada8dabf7eac666ad118790110b6ee383bc93d60 Mon Sep 17 00:00:00 2001 From: Ganendra Afrasya Date: Thu, 28 Nov 2019 18:48:43 +0700 Subject: [PATCH 036/189] Add right margin on score multiplier text --- osu.Game/Screens/Select/FooterButtonMods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 8dc0a2a132..fbeda8f18e 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -54,6 +54,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(weight: FontWeight.Bold), + Margin = new MarginPadding { Right = 10 } } }, AutoSizeAxes = Axes.Both, From af5c5a3000beb54796f9f356d4cd59b804a500a4 Mon Sep 17 00:00:00 2001 From: Ganendra Afrasya Date: Thu, 28 Nov 2019 20:06:15 +0700 Subject: [PATCH 037/189] Apply reviews Also rename the class name to match with the others --- ...onMods.cs => TestSceneFooterButtonMods.cs} | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestCaseFooterButtonMods.cs => TestSceneFooterButtonMods.cs} (81%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs similarity index 81% rename from osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 0c437800bc..e8d3475a11 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestCaseFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -11,7 +11,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.UserInterface { - public class TestCaseFooterButtonMods : OsuTestScene + public class TestSceneFooterButtonMods : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly TestFooterButtonMods footerButtonMods; - public TestCaseFooterButtonMods() + public TestSceneFooterButtonMods() { Add(footerButtonMods = new TestFooterButtonMods()); } @@ -29,24 +29,24 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestIncrementMultiplier() { AddStep(@"Add Hidden", () => changeMods(new Mod[] { new OsuModHidden() })); - AddAssert(@"Check Hidden multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.06x")); + AddAssert(@"Check Hidden multiplier", () => footerButtonMods.MultiplierText.Text == @"1.06x"); AddStep(@"Add HardRock", () => changeMods(new Mod[] { new OsuModHidden() })); - AddAssert(@"Check HardRock multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.06x")); + AddAssert(@"Check HardRock multiplier", () => footerButtonMods.MultiplierText.Text == @"1.06x"); AddStep(@"Add DoubleTime", () => changeMods(new Mod[] { new OsuModDoubleTime() })); - AddAssert(@"Check DoubleTime multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.12x")); + AddAssert(@"Check DoubleTime multiplier", () => footerButtonMods.MultiplierText.Text == @"1.12x"); AddStep(@"Add multiple Mods", () => changeMods(new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHidden() })); - AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"1.26x")); + AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text == @"1.26x"); } [Test] public void TestDecrementMultiplier() { AddStep(@"Add Easy", () => changeMods(new Mod[] { new OsuModEasy() })); - AddAssert(@"Check Easy multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"0.50x")); + AddAssert(@"Check Easy multiplier", () => footerButtonMods.MultiplierText.Text == @"0.50x"); AddStep(@"Add NoFail", () => changeMods(new Mod[] { new OsuModNoFail() })); - AddAssert(@"Check NoFail multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"0.50x")); + AddAssert(@"Check NoFail multiplier", () => footerButtonMods.MultiplierText.Text == @"0.50x"); AddStep(@"Add Multiple Mods", () => changeMods(new Mod[] { new OsuModEasy(), new OsuModNoFail() })); - AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text.Equals(@"0.25x")); + AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text == @"0.25x"); } [Test] @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep(@"Add mods", () => changeMods(new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() })); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddAssert(@"Check empty multiplier", () => footerButtonMods.MultiplierText.Text.Equals(string.Empty)); + AddAssert(@"Check empty multiplier", () => string.IsNullOrEmpty(footerButtonMods.MultiplierText.Text)); } private void changeMods(IReadOnlyList mods) From 88ec0c14862a862750060e0717324cf2c03606f9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Dec 2019 18:49:58 +0100 Subject: [PATCH 038/189] Add missing async content loading logic to NewsOverlay --- osu.Game/Overlays/NewsOverlay.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index aadca8883e..db4b118bf8 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.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.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,7 +17,6 @@ namespace osu.Game.Overlays { private NewsHeader header; - //ReSharper disable NotAccessedField.Local private Container content; public readonly Bindable Current = new Bindable(null); @@ -59,6 +59,21 @@ namespace osu.Game.Overlays Current.TriggerChange(); } + private CancellationTokenSource loadChildCancellation; + + protected void LoadAndShowChild(NewsContent newContent) + { + content.FadeTo(0.2f, 300, Easing.OutQuint); + + loadChildCancellation?.Cancel(); + + LoadComponentAsync(newContent, c => + { + content.Child = c; + content.FadeIn(300, Easing.OutQuint); + }, (loadChildCancellation = new CancellationTokenSource()).Token); + } + public void ShowFrontPage() { Current.Value = null; From 8012b21ffae925fc610fd23d96b334ea50d37e8f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Dec 2019 20:04:37 +0900 Subject: [PATCH 039/189] Extract legacy sound type enum --- .../Beatmaps/Legacy/LegacyHitSoundType.cs | 14 +++++++++++ .../Objects/Legacy/ConvertHitObjectParser.cs | 24 ++++++------------- 2 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs new file mode 100644 index 0000000000..69adbf8f67 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs @@ -0,0 +1,14 @@ +using System; + +namespace osu.Game.Beatmaps.Legacy +{ + [Flags] + internal enum LegacyHitSoundType + { + None = 0, + Normal = 1, + Whistle = 2, + Finish = 4, + Clap = 8 + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b5b1e26486..c22955cbf1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Objects.Legacy bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); type &= ~ConvertHitObjectType.NewCombo; - var soundType = (LegacySoundType)Parsing.ParseInt(split[4]); + var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); HitObject result = null; @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } // Populate node sound types with the default hit object sound type - var nodeSoundTypes = new List(); + var nodeSoundTypes = new List(); for (int i = 0; i < nodes; i++) nodeSoundTypes.Add(soundType); @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Objects.Legacy break; int.TryParse(adds[i], out var sound); - nodeSoundTypes[i] = (LegacySoundType)sound; + nodeSoundTypes[i] = (LegacyHitSoundType)sound; } } @@ -333,7 +333,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The hold end time. protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime); - private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) + private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) { // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario if (!string.IsNullOrEmpty(bankInfo.Filename)) @@ -359,7 +359,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } }; - if (type.HasFlag(LegacySoundType.Finish)) + if (type.HasFlag(LegacyHitSoundType.Finish)) { soundTypes.Add(new LegacyHitSampleInfo { @@ -370,7 +370,7 @@ namespace osu.Game.Rulesets.Objects.Legacy }); } - if (type.HasFlag(LegacySoundType.Whistle)) + if (type.HasFlag(LegacyHitSoundType.Whistle)) { soundTypes.Add(new LegacyHitSampleInfo { @@ -381,7 +381,7 @@ namespace osu.Game.Rulesets.Objects.Legacy }); } - if (type.HasFlag(LegacySoundType.Clap)) + if (type.HasFlag(LegacyHitSoundType.Clap)) { soundTypes.Add(new LegacyHitSampleInfo { @@ -430,15 +430,5 @@ namespace osu.Game.Rulesets.Objects.Legacy Path.ChangeExtension(Filename, null) }; } - - [Flags] - private enum LegacySoundType - { - None = 0, - Normal = 1, - Whistle = 2, - Finish = 4, - Clap = 8 - } } } From e3f925f69afd6bb69081729c5f419c2af43208bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Dec 2019 20:19:16 +0900 Subject: [PATCH 040/189] Extract legacy hitobject type enum --- .../Beatmaps/Legacy/LegacyHitObjectType.cs | 15 +++++++++++++++ .../Objects/Legacy/ConvertHitObjectParser.cs | 19 ++++++++++--------- .../Objects/Legacy/ConvertHitObjectType.cs | 18 ------------------ 3 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs delete mode 100644 osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs new file mode 100644 index 0000000000..9223f7df31 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -0,0 +1,15 @@ +using System; + +namespace osu.Game.Beatmaps.Legacy +{ + [Flags] + internal enum LegacyHitObjectType + { + Circle = 1, + Slider = 1 << 1, + NewCombo = 1 << 2, + Spinner = 1 << 3, + ComboOffset = (1 << 4) | (1 << 5) | (1 << 6), + Hold = 1 << 7 + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c22955cbf1..b83c67acff 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; using osu.Framework.MathUtils; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Rulesets.Objects.Legacy { @@ -46,27 +47,27 @@ namespace osu.Game.Rulesets.Objects.Legacy double startTime = Parsing.ParseDouble(split[2]) + Offset; - ConvertHitObjectType type = (ConvertHitObjectType)Parsing.ParseInt(split[3]); + LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]); - int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; - type &= ~ConvertHitObjectType.ComboOffset; + int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; + type &= ~LegacyHitObjectType.ComboOffset; - bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); - type &= ~ConvertHitObjectType.NewCombo; + bool combo = type.HasFlag(LegacyHitObjectType.NewCombo); + type &= ~LegacyHitObjectType.NewCombo; var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); HitObject result = null; - if (type.HasFlag(ConvertHitObjectType.Circle)) + if (type.HasFlag(LegacyHitObjectType.Circle)) { result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); } - else if (type.HasFlag(ConvertHitObjectType.Slider)) + else if (type.HasFlag(LegacyHitObjectType.Slider)) { PathType pathType = PathType.Catmull; double? length = null; @@ -186,7 +187,7 @@ namespace osu.Game.Rulesets.Objects.Legacy // The samples are played when the slider ends, which is the last node result.Samples = nodeSamples[nodeSamples.Count - 1]; } - else if (type.HasFlag(ConvertHitObjectType.Spinner)) + else if (type.HasFlag(LegacyHitObjectType.Spinner)) { double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); @@ -195,7 +196,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); } - else if (type.HasFlag(ConvertHitObjectType.Hold)) + else if (type.HasFlag(LegacyHitObjectType.Hold)) { // Note: Hold is generated by BMS converts diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs deleted file mode 100644 index eab37b682c..0000000000 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs +++ /dev/null @@ -1,18 +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; - -namespace osu.Game.Rulesets.Objects.Legacy -{ - [Flags] - internal enum ConvertHitObjectType - { - Circle = 1, - Slider = 1 << 1, - NewCombo = 1 << 2, - Spinner = 1 << 3, - ComboOffset = (1 << 4) | (1 << 5) | (1 << 6), - Hold = 1 << 7 - } -} From 3c18872a16605e4dc87261a520cdbc5ecb27e921 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Dec 2019 20:19:31 +0900 Subject: [PATCH 041/189] Extract legacy effect flags enum --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 15 ++++----------- osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 838b1c2f07..ff4e6503ee 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Beatmaps.Formats { @@ -358,9 +359,9 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 8) { - EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine); + LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); + kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine); } string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); @@ -448,13 +449,5 @@ namespace osu.Game.Beatmaps.Formats private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0); protected virtual TimingControlPoint CreateTimingControlPoint() => new TimingControlPoint(); - - [Flags] - internal enum EffectFlags - { - None = 0, - Kiai = 1, - OmitFirstBarLine = 8 - } } } diff --git a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs new file mode 100644 index 0000000000..ce141031fd --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace osu.Game.Beatmaps.Legacy +{ + [Flags] + internal enum LegacyEffectFlags + { + None = 0, + Kiai = 1, + OmitFirstBarLine = 8 + } +} From c378e525dae265a0d513090a002e2cc235a04415 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Dec 2019 20:23:15 +0900 Subject: [PATCH 042/189] Extract the rest of legacy enums --- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++-- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 37 ------------------- .../Formats/LegacyStoryboardDecoder.cs | 11 +++--- osu.Game/Beatmaps/Legacy/LegacyEventType.cs | 13 +++++++ osu.Game/Beatmaps/Legacy/LegacyOrigins.cs | 16 ++++++++ osu.Game/Beatmaps/Legacy/LegacySampleBank.cs | 10 +++++ osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs | 10 +++++ .../Objects/Legacy/ConvertHitObjectParser.cs | 4 +- 8 files changed, 61 insertions(+), 48 deletions(-) create mode 100644 osu.Game/Beatmaps/Legacy/LegacyEventType.cs create mode 100644 osu.Game/Beatmaps/Legacy/LegacyOrigins.cs create mode 100644 osu.Game/Beatmaps/Legacy/LegacySampleBank.cs create mode 100644 osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index ff4e6503ee..1b24e2953b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -294,22 +294,22 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - if (!Enum.TryParse(split[0], out EventType type)) + if (!Enum.TryParse(split[0], out LegacyEventType type)) throw new InvalidDataException($@"Unknown event type: {split[0]}"); switch (type) { - case EventType.Background: + case LegacyEventType.Background: string bgFilename = split[2].Trim('"'); beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(bgFilename); break; - case EventType.Video: + case LegacyEventType.Video: string videoFilename = split[2].Trim('"'); beatmap.BeatmapInfo.Metadata.VideoFile = FileSafety.PathStandardise(videoFilename); break; - case EventType.Break: + case LegacyEventType.Break: double start = getOffsetTime(Parsing.ParseDouble(split[1])); var breakEvent = new BreakPeriod diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 2b914669cb..1c2ca4b6f8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -148,46 +148,9 @@ namespace osu.Game.Beatmaps.Formats Fonts } - internal enum LegacySampleBank - { - None = 0, - Normal = 1, - Soft = 2, - Drum = 3 - } - internal enum EventType - { - Background = 0, - Video = 1, - Break = 2, - Colour = 3, - Sprite = 4, - Sample = 5, - Animation = 6 - } - internal enum LegacyOrigins - { - TopLeft, - Centre, - CentreLeft, - TopRight, - BottomCentre, - TopCentre, - Custom, - CentreRight, - BottomLeft, - BottomRight - } - internal enum StoryLayer - { - Background = 0, - Fail = 1, - Pass = 2, - Foreground = 3 - } internal class LegacyDifficultyControlPoint : DifficultyControlPoint { diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index f94ab3f27b..b8323edc23 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.IO.File; using osu.Game.IO; using osu.Game.Storyboards; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Beatmaps.Formats { @@ -83,12 +84,12 @@ namespace osu.Game.Beatmaps.Formats { storyboardSprite = null; - if (!Enum.TryParse(split[0], out EventType type)) + if (!Enum.TryParse(split[0], out LegacyEventType type)) throw new InvalidDataException($@"Unknown event type: {split[0]}"); switch (type) { - case EventType.Sprite: + case LegacyEventType.Sprite: { var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); @@ -100,7 +101,7 @@ namespace osu.Game.Beatmaps.Formats break; } - case EventType.Animation: + case LegacyEventType.Animation: { var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); @@ -115,7 +116,7 @@ namespace osu.Game.Beatmaps.Formats break; } - case EventType.Sample: + case LegacyEventType.Sample: { var time = double.Parse(split[1], CultureInfo.InvariantCulture); var layer = parseLayer(split[2]); @@ -271,7 +272,7 @@ namespace osu.Game.Beatmaps.Formats } } - private string parseLayer(string value) => Enum.Parse(typeof(StoryLayer), value).ToString(); + private string parseLayer(string value) => Enum.Parse(typeof(LegacyStoryLayer), value).ToString(); private Anchor parseOrigin(string value) { diff --git a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs new file mode 100644 index 0000000000..57b1e6c29f --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs @@ -0,0 +1,13 @@ +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacyEventType + { + Background = 0, + Video = 1, + Break = 2, + Colour = 3, + Sprite = 4, + Sample = 5, + Animation = 6 + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs new file mode 100644 index 0000000000..93c8920761 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs @@ -0,0 +1,16 @@ +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacyOrigins + { + TopLeft, + Centre, + CentreLeft, + TopRight, + BottomCentre, + TopCentre, + Custom, + CentreRight, + BottomLeft, + BottomRight + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs new file mode 100644 index 0000000000..0d54998d15 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs @@ -0,0 +1,10 @@ +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacySampleBank + { + None = 0, + Normal = 1, + Soft = 2, + Drum = 3 + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs new file mode 100644 index 0000000000..509f39f830 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -0,0 +1,10 @@ +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacyStoryLayer + { + Background = 0, + Fail = 1, + Pass = 2, + Foreground = 3 + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b83c67acff..d3682de73f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -232,8 +232,8 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); - var bank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[0]); - var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[1]); + var bank = (LegacySampleBank)Parsing.ParseInt(split[0]); + var addbank = (LegacySampleBank)Parsing.ParseInt(split[1]); string stringBank = bank.ToString().ToLowerInvariant(); if (stringBank == @"none") From 6c1ae3bc8ac77bd1babbd0f0395071fd583a905a Mon Sep 17 00:00:00 2001 From: Albie Date: Tue, 10 Dec 2019 16:59:31 +0000 Subject: [PATCH 043/189] add tests --- .../Background/TestSceneUserDimContainer.cs | 17 +++++++++++++++++ .../Graphics/Containers/UserDimContainer.cs | 9 +++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index f858174ff2..ecea80d6cc 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -165,6 +165,21 @@ namespace osu.Game.Tests.Visual.Background AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } + /// + /// Ensure is able to disable user-defined display settings. + /// + [Test] + public void DisableUserDisplaySettingsTest() + { + performFullSetup(); + AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); + AddStep("Set user dim to max", () => player.DimmableStoryboard.) + waitForDim(); + AddStep("Turn on IgnoreUserSettings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); + waitForDim(); + AddAssert("Check the background is undimmed", () => player.IsBackgroundUndimmed()); + } + /// /// Ensure is properly accepting user-defined visual changes for a storyboard. /// @@ -352,6 +367,8 @@ namespace osu.Game.Tests.Visual.Background public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; + public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).Colour == Color4.White; + // Whether or not the player should be allowed to load. public bool BlockLoad; diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 42a25a79b1..74d922704e 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; +using osuTK.Graphics; namespace osu.Game.Graphics.Containers { @@ -22,7 +23,7 @@ namespace osu.Game.Graphics.Containers public readonly Bindable EnableUserDim = new Bindable(true); /// - /// Whether or not user-configured settings relating to visibility of elements should be ignored + /// Whether or not user-configured settings relating to brightness of elements should be ignored /// public readonly Bindable IgnoreUserSettings = new Bindable(); @@ -36,14 +37,14 @@ namespace osu.Game.Graphics.Containers /// public bool ContentDisplayed { get; private set; } + public double DimLevel => EnableUserDim.Value && !IgnoreUserSettings.Value ? UserDimLevel.Value : 0; + protected Bindable UserDimLevel { get; private set; } protected Bindable ShowStoryboard { get; private set; } protected Bindable ShowVideo { get; private set; } - protected double DimLevel => EnableUserDim.Value ? UserDimLevel.Value : 0; - protected override Container Content => dimContent; private Container dimContent { get; } @@ -90,7 +91,7 @@ namespace osu.Game.Graphics.Containers ContentDisplayed = ShowDimContent; dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); - dimContent.FadeColour(OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint); + dimContent.FadeColour(IgnoreUserSettings.Value ? OsuColour.Gray(1 - (float)DimLevel) : Color4.White, BACKGROUND_FADE_DURATION, Easing.OutQuint); } } } From 479acdcb5b670b0644a2755cd3b93396b3a95764 Mon Sep 17 00:00:00 2001 From: Albie Date: Tue, 10 Dec 2019 17:06:34 +0000 Subject: [PATCH 044/189] fix build bug --- osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index ecea80d6cc..8c7948ebef 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -173,7 +173,6 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(); AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); - AddStep("Set user dim to max", () => player.DimmableStoryboard.) waitForDim(); AddStep("Turn on IgnoreUserSettings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); waitForDim(); From 12fb17572c1ce4ad6cb8f1e37b6755c11c13dc8f Mon Sep 17 00:00:00 2001 From: Albie Date: Tue, 10 Dec 2019 18:16:34 +0000 Subject: [PATCH 045/189] cleanup test logic --- .../Background/TestSceneUserDimContainer.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index 5b7900bad4..28cc7a8532 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -148,6 +148,20 @@ namespace osu.Game.Tests.Visual.Background AddAssert("Background is visible", () => songSelect.IsBackgroundVisible()); } + /// + /// Ensure is able to disable user-defined display settings. + /// + [Test] + public void DisableUserDisplaySettingsTest() + { + performFullSetup(); + AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); + waitForDim(); + AddStep("Turn on IgnoreUserSettings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); + waitForDim(); + AddAssert("Check the background is undimmed", () => player.IsBackgroundUndimmed()); + } + /// /// Ensure is properly accepting user-defined visual changes for a background. /// @@ -165,20 +179,6 @@ namespace osu.Game.Tests.Visual.Background AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } - /// - /// Ensure is able to disable user-defined display settings. - /// - [Test] - public void DisableUserDisplaySettingsTest() - { - performFullSetup(); - AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); - waitForDim(); - AddStep("Turn on IgnoreUserSettings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); - waitForDim(); - AddAssert("Check the background is undimmed", () => player.IsBackgroundUndimmed()); - } - /// /// Ensure is properly accepting user-defined visual changes for a storyboard. /// @@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; - public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).Colour == Color4.White; + public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White; // Whether or not the player should be allowed to load. public bool BlockLoad; From 83b2e0525e9055dae18b7eb7396a5804dbd21d39 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 11 Dec 2019 07:02:51 +0000 Subject: [PATCH 046/189] further fixes, not perfect yet --- .../Visual/Background/TestSceneUserDimContainer.cs | 6 ++---- osu.Game/Graphics/Containers/UserDimContainer.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index 28cc7a8532..f867b98fda 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -155,9 +155,7 @@ namespace osu.Game.Tests.Visual.Background public void DisableUserDisplaySettingsTest() { performFullSetup(); - AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); - waitForDim(); - AddStep("Turn on IgnoreUserSettings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); + AddStep("Start ignoring user settings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); waitForDim(); AddAssert("Check the background is undimmed", () => player.IsBackgroundUndimmed()); } @@ -367,7 +365,7 @@ namespace osu.Game.Tests.Visual.Background public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; - public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White; + public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == OsuColour.Gray(1); // Whether or not the player should be allowed to load. public bool BlockLoad; diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 74d922704e..bddbbca0ea 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -91,7 +91,7 @@ namespace osu.Game.Graphics.Containers ContentDisplayed = ShowDimContent; dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); - dimContent.FadeColour(IgnoreUserSettings.Value ? OsuColour.Gray(1 - (float)DimLevel) : Color4.White, BACKGROUND_FADE_DURATION, Easing.OutQuint); + dimContent.FadeColour(IgnoreUserSettings.Value ? Color4.White : OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint); } } } From e7a06aeadcefa773cb13efb2f39916b0a66bfd89 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 11 Dec 2019 14:32:43 +0100 Subject: [PATCH 047/189] Update NewsOverlay visual tests to expose LoadAndShowChild() for testing purposes --- osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 546f6ac182..98f90f2daa 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -2,22 +2,28 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Overlays; +using osu.Game.Overlays.News; namespace osu.Game.Tests.Visual.Online { public class TestSceneNewsOverlay : OsuTestScene { - private NewsOverlay news; + private TestNewsOverlay news; protected override void LoadComplete() { base.LoadComplete(); - Add(news = new NewsOverlay()); + Add(news = new TestNewsOverlay()); AddStep(@"Show", news.Show); AddStep(@"Hide", news.Hide); AddStep(@"Show front page", () => news.ShowFrontPage()); AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); } + + private class TestNewsOverlay : NewsOverlay + { + public new void LoadAndShowChild(NewsContent content) => base.LoadAndShowChild(content); + } } } From caa9286a9091f2482ba4ba5b6879f4894cc675ad Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 11 Dec 2019 17:39:40 +0000 Subject: [PATCH 048/189] update tests, change binding and reduce lines in cinema mod --- .../Background/TestSceneUserDimContainer.cs | 15 +++++++++++---- osu.Game/Graphics/Containers/UserDimContainer.cs | 14 +++++++++++--- osu.Game/Rulesets/Mods/ModCinema.cs | 2 -- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index f867b98fda..fdfde9cc2f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -155,9 +155,18 @@ namespace osu.Game.Tests.Visual.Background public void DisableUserDisplaySettingsTest() { performFullSetup(); - AddStep("Start ignoring user settings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); + createFakeStoryboard(); + AddStep("Enable Storyboard", () => + { + player.ReplacesBackground.Value = true; + player.StoryboardEnabled.Value = true; + }); + AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); + AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); waitForDim(); - AddAssert("Check the background is undimmed", () => player.IsBackgroundUndimmed()); + AddAssert("Ignore User Settings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); + waitForDim(); + AddAssert("User dim settings ignored", () => !player.DimmableStoryboard.EnableUserDim.Value && player.DimmableStoryboard.DimLevel == 0); } /// @@ -365,8 +374,6 @@ namespace osu.Game.Tests.Visual.Background public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; - public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == OsuColour.Gray(1); - // Whether or not the player should be allowed to load. public bool BlockLoad; diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index bddbbca0ea..ae4e5557be 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.Containers /// public bool ContentDisplayed { get; private set; } - public double DimLevel => EnableUserDim.Value && !IgnoreUserSettings.Value ? UserDimLevel.Value : 0; + public double DimLevel => EnableUserDim.Value ? UserDimLevel.Value : 0; protected Bindable UserDimLevel { get; private set; } @@ -69,7 +69,7 @@ namespace osu.Game.Graphics.Containers ShowStoryboard.ValueChanged += _ => UpdateVisuals(); ShowVideo.ValueChanged += _ => UpdateVisuals(); StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); - IgnoreUserSettings.ValueChanged += _ => UpdateVisuals(); + IgnoreUserSettings.ValueChanged += _ => updateSettings(); } protected override void LoadComplete() @@ -91,7 +91,15 @@ namespace osu.Game.Graphics.Containers ContentDisplayed = ShowDimContent; dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); - dimContent.FadeColour(IgnoreUserSettings.Value ? Color4.White : OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint); + dimContent.FadeColour(OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint); + } + + /// + /// Invoked when the IgnoreUserSettings bindable is changed + /// + private void updateSettings() + { + EnableUserDim.Value = !IgnoreUserSettings.Value; } } } diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 77bf80b149..5faa2f4f3e 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Mods public void ApplyToPlayer(Player player) { player.Background.EnableUserDim.Value = false; - player.DimmableVideo.EnableUserDim.Value = false; - player.DimmableStoryboard.EnableUserDim.Value = false; player.DimmableVideo.IgnoreUserSettings.Value = true; player.DimmableStoryboard.IgnoreUserSettings.Value = true; From 2ca722423b76898ca121bf3dee1444f552f03d4a Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 11 Dec 2019 18:58:14 +0000 Subject: [PATCH 049/189] remove uneccesary using statement --- osu.Game/Graphics/Containers/UserDimContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index ae4e5557be..f83b85e023 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; -using osuTK.Graphics; namespace osu.Game.Graphics.Containers { From 663405d17d4ffeb6e74063db2fddd33f6715bc72 Mon Sep 17 00:00:00 2001 From: Albie Date: Wed, 11 Dec 2019 19:55:45 +0000 Subject: [PATCH 050/189] reduce test length and fix the poorly worded description --- .../Background/TestSceneUserDimContainer.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index fdfde9cc2f..52f8fe0a2e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -119,11 +119,7 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(); createFakeStoryboard(); - AddStep("Enable Storyboard", () => - { - player.ReplacesBackground.Value = true; - player.StoryboardEnabled.Value = true; - }); + enableStoryboard(); waitForDim(); AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible); AddStep("Disable Storyboard", () => @@ -149,22 +145,17 @@ namespace osu.Game.Tests.Visual.Background } /// - /// Ensure is able to disable user-defined display settings. + /// Ensure can disable user-defined display settings. /// [Test] public void DisableUserDisplaySettingsTest() { performFullSetup(); createFakeStoryboard(); - AddStep("Enable Storyboard", () => - { - player.ReplacesBackground.Value = true; - player.StoryboardEnabled.Value = true; - }); + enableStoryboard(); AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); - AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); waitForDim(); - AddAssert("Ignore User Settings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); + AddAssert("Ignore user settings", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true); waitForDim(); AddAssert("User dim settings ignored", () => !player.DimmableStoryboard.EnableUserDim.Value && player.DimmableStoryboard.DimLevel == 0); } @@ -194,11 +185,7 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(); createFakeStoryboard(); - AddStep("Enable Storyboard", () => - { - player.ReplacesBackground.Value = true; - player.StoryboardEnabled.Value = true; - }); + enableStoryboard(); AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); waitForDim(); @@ -285,6 +272,12 @@ namespace osu.Game.Tests.Visual.Background }); }); + private void enableStoryboard() => AddStep("Enable Storyboard", () => + { + player.ReplacesBackground.Value = true; + player.StoryboardEnabled.Value = true; + }); + private void performFullSetup(bool allowPause = false) { setupUserSettings(); From e4297ffeaded24ea61f8fd188e7fb92215d70674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 15:09:55 +0900 Subject: [PATCH 051/189] Hide HUD in a better way --- osu.Game/Rulesets/Mods/ModCinema.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 5faa2f4f3e..5262813b08 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -30,8 +30,8 @@ namespace osu.Game.Rulesets.Mods public void ApplyToHUD(HUDOverlay overlay) { - overlay.AlwaysPresent = true; - overlay.Hide(); + overlay.ShowHud.Value = false; + overlay.ShowHud.Disabled = true; } public void ApplyToPlayer(Player player) From 99280db69487c1f0b8542031c8d6037fe875084e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 15:16:34 +0900 Subject: [PATCH 052/189] Add note about AlwaysPresent requirement --- osu.Game/Rulesets/Mods/ModCinema.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 5262813b08..1e4cf66711 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Mods { drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); + // AlwaysPresent required for hitsounds drawableRuleset.Playfield.AlwaysPresent = true; drawableRuleset.Playfield.Hide(); } From d15f49f60f33c5beba335b75f0e0b8d40fc8ee10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 15:14:59 +0900 Subject: [PATCH 053/189] Also hide the break overlay --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 ++ osu.Game/Screens/Play/Player.cs | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 1e4cf66711..3487d49e08 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -41,6 +41,8 @@ namespace osu.Game.Rulesets.Mods player.DimmableVideo.IgnoreUserSettings.Value = true; player.DimmableStoryboard.IgnoreUserSettings.Value = true; + + player.BreakOverlay.Hide(); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7abd60b3c1..1d1252063f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Play private SampleChannel sampleRestart; - private BreakOverlay breakOverlay; + public BreakOverlay BreakOverlay; protected ScoreProcessor ScoreProcessor { get; private set; } protected DrawableRuleset DrawableRuleset { get; private set; } @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); - breakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState(); + BreakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState(); } private void addUnderlayComponents(Container target) @@ -183,7 +183,7 @@ namespace osu.Game.Screens.Play { target.AddRange(new[] { - breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -248,7 +248,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value - && !breakOverlay.IsBreakTime.Value; + && !BreakOverlay.IsBreakTime.Value; private WorkingBeatmap loadBeatmap() { @@ -477,7 +477,7 @@ namespace osu.Game.Screens.Play PauseOverlay.Hide(); // breaks and time-based conditions may allow instant resume. - if (breakOverlay.IsBreakTime.Value) + if (BreakOverlay.IsBreakTime.Value) completeResume(); else DrawableRuleset.RequestResume(completeResume); From be000e13e445ff6c63c0b43b3551e5eb34607992 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Dec 2019 20:44:45 +0900 Subject: [PATCH 054/189] Implement initial legacy beatmap encoding support --- .../Formats/LegacyBeatmapEncoderTest.cs | 142 +++++++ .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 375 ++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs create mode 100644 osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs new file mode 100644 index 0000000000..c4a3877c1c --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Tests.Resources; +using osuTK; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyBeatmapEncoderTest + { + private const string normal = "Soleily - Renatus (Gamu) [Insane].osu"; + + [Test] + public void TestDecodeMetadata() + { + var beatmap = decode(normal); + var meta = beatmap.BeatmapInfo.Metadata; + Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID); + Assert.AreEqual("Soleily", meta.Artist); + Assert.AreEqual("Soleily", meta.ArtistUnicode); + Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); + Assert.AreEqual("Gamu", meta.AuthorString); + Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile); + Assert.AreEqual(164471, meta.PreviewTime); + Assert.AreEqual(string.Empty, meta.Source); + Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags); + Assert.AreEqual("Renatus", meta.Title); + Assert.AreEqual("Renatus", meta.TitleUnicode); + } + + [Test] + public void TestDecodeGeneral() + { + var beatmap = decode(normal); + var beatmapInfo = beatmap.BeatmapInfo; + Assert.AreEqual(0, beatmapInfo.AudioLeadIn); + Assert.AreEqual(false, beatmapInfo.Countdown); + Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); + Assert.AreEqual(false, beatmapInfo.SpecialStyle); + Assert.IsTrue(beatmapInfo.RulesetID == 0); + Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); + Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); + } + + [Test] + public void TestDecodeEditor() + { + var beatmap = decode(normal); + var beatmapInfo = beatmap.BeatmapInfo; + + int[] expectedBookmarks = + { + 11505, 22054, 32604, 43153, 53703, 64252, 74802, 85351, + 95901, 106450, 116999, 119637, 130186, 140735, 151285, + 161834, 164471, 175020, 185570, 196119, 206669, 209306 + }; + Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); + for (int i = 0; i < expectedBookmarks.Length; i++) + Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); + Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); + Assert.AreEqual(4, beatmapInfo.BeatDivisor); + Assert.AreEqual(4, beatmapInfo.GridSize); + Assert.AreEqual(2, beatmapInfo.TimelineZoom); + } + + [Test] + public void TestDecodeDifficulty() + { + var beatmap = decode(normal); + var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + Assert.AreEqual(6.5f, difficulty.DrainRate); + Assert.AreEqual(4, difficulty.CircleSize); + Assert.AreEqual(8, difficulty.OverallDifficulty); + Assert.AreEqual(9, difficulty.ApproachRate); + Assert.AreEqual(1.8, difficulty.SliderMultiplier); + Assert.AreEqual(2, difficulty.SliderTickRate); + } + + [Test] + public void TestDecodeHitObjects() + { + var beatmap = decode(normal); + + var curveData = beatmap.HitObjects[0] as IHasCurve; + var positionData = beatmap.HitObjects[0] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.IsNotNull(curveData); + Assert.AreEqual(new Vector2(192, 168), positionData.Position); + Assert.AreEqual(956, beatmap.HitObjects[0].StartTime); + Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); + + positionData = beatmap.HitObjects[1] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.AreEqual(new Vector2(304, 56), positionData.Position); + Assert.AreEqual(1285, beatmap.HitObjects[1].StartTime); + Assert.IsTrue(beatmap.HitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); + } + + private Beatmap decode(string filename) + { + decode(filename, out Beatmap jsonDecoded); + return jsonDecoded; + } + + private Beatmap decode(string filename, out Beatmap decoded) + { + using (var stream = TestResources.OpenResource(filename)) + using (var sr = new LineBufferedReader(stream)) + { + var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr); + + using (var ms = new MemoryStream()) + using (var sw = new StreamWriter(ms)) + using (var sr2 = new LineBufferedReader(ms)) + { + new LegacyBeatmapEncoder(legacyDecoded).Encode(sw); + sw.Flush(); + + ms.Position = 0; + + string result = sr2.ReadToEnd(); + + ms.Position = 0; + + decoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2); + return legacyDecoded; + } + } + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs new file mode 100644 index 0000000000..1dc29ba5db --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -0,0 +1,375 @@ +// 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.IO; +using System.Linq; +using System.Text; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Beatmaps.Formats +{ + public class LegacyBeatmapEncoder + { + public const int LATEST_VERSION = 14234; + + private readonly IBeatmap beatmap; + + public LegacyBeatmapEncoder(IBeatmap beatmap) + { + this.beatmap = beatmap; + + if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3) + throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap)); + } + + public void Encode(TextWriter writer) + { + writer.WriteLine($"osu file format v{LATEST_VERSION}"); + + writer.WriteLine(); + handleGeneral(writer); + + writer.WriteLine(); + handleEditor(writer); + + writer.WriteLine(); + handleMetadata(writer); + + writer.WriteLine(); + handleDifficulty(writer); + + writer.WriteLine(); + handleEvents(writer); + + writer.WriteLine(); + handleTimingPoints(writer); + + writer.WriteLine(); + handleHitObjects(writer); + } + + private void handleGeneral(TextWriter writer) + { + writer.WriteLine("[General]"); + + writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); + writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); + writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); + // Todo: Not all countdown types are supported by lazer yet + writer.WriteLine(FormattableString.Invariant($"Countdown: {(beatmap.BeatmapInfo.Countdown ? "1" : "0")}")); + writer.WriteLine(FormattableString.Invariant($"SampleSet: {(int)toLegacySampleBank(beatmap.ControlPointInfo.SamplePoints[0].SampleBank)}")); + writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); + writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}")); + writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? "1" : "0")}")); + // if (beatmap.BeatmapInfo.UseSkinSprites) + // writer.WriteLine(@"UseSkinSprites: 1"); + // if (b.AlwaysShowPlayfield) + // writer.WriteLine(@"AlwaysShowPlayfield: 1"); + // if (b.OverlayPosition != OverlayPosition.NoChange) + // writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition); + // if (!string.IsNullOrEmpty(b.SkinPreference)) + // writer.WriteLine(@"SkinPreference:" + b.SkinPreference); + // if (b.EpilepsyWarning) + // writer.WriteLine(@"EpilepsyWarning: 1"); + // if (b.CountdownOffset > 0) + // writer.WriteLine(@"CountdownOffset: " + b.CountdownOffset.ToString()); + if (beatmap.BeatmapInfo.RulesetID == 3) + writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? "1" : "0")}")); + writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? "1" : "0")}")); + // if (b.SamplesMatchPlaybackRate) + // writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); + } + + private void handleEditor(TextWriter writer) + { + writer.WriteLine("[Editor]"); + + if (beatmap.BeatmapInfo.Bookmarks.Length > 0) + writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); + writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.BeatmapInfo.DistanceSpacing}")); + writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); + writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.BeatmapInfo.GridSize}")); + writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); + } + + private void handleMetadata(TextWriter writer) + { + writer.WriteLine("[Metadata]"); + + writer.WriteLine(FormattableString.Invariant($"Title: {beatmap.Metadata.Title}")); + writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}")); + writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}")); + writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}")); + writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.AuthorString}")); + writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.Metadata.Artist}")); + writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); + writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); + writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}")); + writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? 0}")); + } + + private void handleDifficulty(TextWriter writer) + { + writer.WriteLine("[Difficulty]"); + + writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.BeatmapInfo.BaseDifficulty.DrainRate}")); + writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}")); + writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}")); + writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}")); + writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}")); + writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}")); + } + + private void handleEvents(TextWriter writer) + { + // Todo: Storyboard events + } + + private void handleTimingPoints(TextWriter writer) + { + if (beatmap.ControlPointInfo.Groups.Count == 0) + return; + + writer.WriteLine("[TimingPoints]"); + + foreach (var group in beatmap.ControlPointInfo.Groups) + { + var timingPoint = group.ControlPoints.OfType().FirstOrDefault(); + var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time); + var samplePoint = beatmap.ControlPointInfo.SamplePointAt(group.Time); + var effectPoint = beatmap.ControlPointInfo.EffectPointAt(group.Time); + + // Convert beat length the legacy format + double beatLength; + if (timingPoint != null) + beatLength = timingPoint.BeatLength; + else + beatLength = -100 / difficultyPoint.SpeedMultiplier; + + // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) + HitSampleInfo tempHitSample = samplePoint.ApplyTo(new HitSampleInfo()); + + // Convert effect flags to the legacy format + LegacyEffectFlags effectFlags = LegacyEffectFlags.None; + if (effectPoint.KiaiMode) + effectFlags |= LegacyEffectFlags.Kiai; + if (effectPoint.OmitFirstBarLine) + effectFlags |= LegacyEffectFlags.OmitFirstBarLine; + + writer.Write(FormattableString.Invariant($"{group.Time},")); + writer.Write(FormattableString.Invariant($"{beatLength},")); + writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(group.Time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); + writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample.Suffix)},")); + writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); + writer.Write(FormattableString.Invariant($"{(timingPoint != null ? "1" : "0")},")); + writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); + writer.Write("\n"); + } + } + + private void handleHitObjects(TextWriter writer) + { + if (beatmap.HitObjects.Count == 0) + return; + + writer.WriteLine("[HitObjects]"); + + foreach (var h in beatmap.HitObjects) + { + switch (beatmap.BeatmapInfo.RulesetID) + { + case 0: + handleOsuHitObject(writer, h); + break; + + case 1: + handleTaikoHitObject(writer, h); + break; + + case 2: + handleCatchHitObject(writer, h); + break; + + case 3: + handleManiaHitObject(writer, h); + break; + } + } + } + + private void handleOsuHitObject(TextWriter writer, HitObject hitObject) + { + var positionData = hitObject as IHasPosition; + var comboData = hitObject as IHasCombo; + + Debug.Assert(positionData != null); + Debug.Assert(comboData != null); + + LegacyHitObjectType hitObjectType = (LegacyHitObjectType)(comboData.ComboOffset << 4); + if (comboData.NewCombo) + hitObjectType |= LegacyHitObjectType.NewCombo; + + if (hitObject is IHasCurve _) + hitObjectType |= LegacyHitObjectType.Slider; + else if (hitObject is IHasEndTime _) + hitObjectType |= LegacyHitObjectType.Spinner; + else + hitObjectType |= LegacyHitObjectType.Circle; + + LegacyHitSoundType soundType = LegacyHitSoundType.Normal; + HitSampleInfo firstAdditionSound = hitObject.Samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL); + if (firstAdditionSound != null) + soundType |= toLegacyHitSound(firstAdditionSound.Name); + + writer.Write(FormattableString.Invariant($"{positionData.X},")); + writer.Write(FormattableString.Invariant($"{positionData.Y},")); + writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); + writer.Write(FormattableString.Invariant($"{(int)hitObjectType},")); + writer.Write(FormattableString.Invariant($"{(int)soundType},")); + + if (hitObject is IHasCurve curveData) + { + for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) + { + PathControlPoint point = curveData.Path.ControlPoints[i]; + + switch (point.Type.Value) + { + case PathType.Bezier: + writer.Write("B|"); + break; + + case PathType.Catmull: + writer.Write("C|"); + break; + + case PathType.PerfectCurve: + writer.Write("P|"); + break; + + case PathType.Linear: + writer.Write("L|"); + break; + } + + writer.Write(FormattableString.Invariant($"{point.Position.Value.X}:{point.Position.Value.Y}")); + writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + } + + writer.Write(FormattableString.Invariant($"{curveData.RepeatCount - 1},")); + writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + LegacyHitSoundType type = LegacyHitSoundType.None; + + foreach (var sample in curveData.NodeSamples[i]) + type |= toLegacyHitSound(sample.Name); + + writer.Write(FormattableString.Invariant($"{(int)type}")); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(getSampleBank(curveData.NodeSamples[i], true)); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + } + else if (hitObject is IHasEndTime endTimeData) + writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); + + writer.Write(getSampleBank(hitObject.Samples)); + writer.Write(Environment.NewLine); + } + + private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) + { + } + + private void handleCatchHitObject(TextWriter writer, HitObject hitObject) + { + } + + private void handleManiaHitObject(TextWriter writer, HitObject hitObject) + { + } + + private string getSampleBank(IList samples, bool banksOnly = false) + { + LegacySampleBank normalBank = toLegacySampleBank(samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); + LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); + + if (addBank == LegacySampleBank.None) + addBank = normalBank; + + string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault()?.Suffix); + string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; + + int volume = samples.First().Volume; + + StringBuilder sb = new StringBuilder(); + + sb.Append(FormattableString.Invariant($"{(int)normalBank}:")); + sb.Append(FormattableString.Invariant($"{(int)addBank}:")); + + if (!banksOnly) + { + sb.Append(FormattableString.Invariant($"{customSampleBank}:")); + sb.Append(FormattableString.Invariant($"{volume}:")); + sb.Append(FormattableString.Invariant($"{sampleFilename}")); + } + + return sb.ToString(); + } + + private LegacyHitSoundType toLegacyHitSound(string hitSoundName) + { + switch (hitSoundName) + { + case HitSampleInfo.HIT_NORMAL: + return LegacyHitSoundType.Normal; + + case HitSampleInfo.HIT_WHISTLE: + return LegacyHitSoundType.Whistle; + + case HitSampleInfo.HIT_FINISH: + return LegacyHitSoundType.Finish; + + case HitSampleInfo.HIT_CLAP: + return LegacyHitSoundType.Clap; + + default: + return LegacyHitSoundType.None; + } + } + + private LegacySampleBank toLegacySampleBank(string sampleBank) + { + switch (sampleBank?.ToLower()) + { + case "normal": + return LegacySampleBank.Normal; + + case "soft": + return LegacySampleBank.Soft; + + case "drum": + return LegacySampleBank.Drum; + + default: + return LegacySampleBank.None; + } + } + + private string toLegacyCustomSampleBank(string sampleSuffix) => string.IsNullOrEmpty(sampleSuffix) ? "0" : sampleSuffix; + } +} From 8f03599a62971f50443cbe772f8f89a96dcc6c6d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:40:33 +0900 Subject: [PATCH 055/189] Write default sampleset by name --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 1dc29ba5db..4011bf2d02 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); // Todo: Not all countdown types are supported by lazer yet writer.WriteLine(FormattableString.Invariant($"Countdown: {(beatmap.BeatmapInfo.Countdown ? "1" : "0")}")); - writer.WriteLine(FormattableString.Invariant($"SampleSet: {(int)toLegacySampleBank(beatmap.ControlPointInfo.SamplePoints[0].SampleBank)}")); + writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank(beatmap.ControlPointInfo.SamplePoints[0].SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? "1" : "0")}")); From c3475a2ddeca1333053f204ebbdeef11bef510eb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:41:13 +0900 Subject: [PATCH 056/189] Write control points in absolute coordinates --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 4011bf2d02..bfbc3babed 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -260,7 +260,7 @@ namespace osu.Game.Beatmaps.Formats break; } - writer.Write(FormattableString.Invariant($"{point.Position.Value.X}:{point.Position.Value.Y}")); + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); } From d1dc3456d101fde973a84ac800c89601ece949ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:42:48 +0900 Subject: [PATCH 057/189] Fix incorrect repeat point count --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index bfbc3babed..23054838f8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -264,7 +264,7 @@ namespace osu.Game.Beatmaps.Formats writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); } - writer.Write(FormattableString.Invariant($"{curveData.RepeatCount - 1},")); + writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); for (int i = 0; i < curveData.NodeSamples.Count; i++) From e09bbf0315643c4d8bd4c6b7ccfa19176780caab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:47:28 +0900 Subject: [PATCH 058/189] Implement background/video/break encoding --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 23054838f8..20706c73c1 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -129,7 +129,11 @@ namespace osu.Game.Beatmaps.Formats private void handleEvents(TextWriter writer) { - // Todo: Storyboard events + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,{beatmap.BeatmapInfo.Metadata.BackgroundFile}")); + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,{beatmap.BeatmapInfo.Metadata.VideoFile}")); + + foreach (var b in beatmap.Breaks) + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); } private void handleTimingPoints(TextWriter writer) From f89042cd0aa3c9e7c120901e2e2d6dd5e0a4706d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:48:22 +0900 Subject: [PATCH 059/189] Add missing section header --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 20706c73c1..8acd8eb17b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -129,6 +129,8 @@ namespace osu.Game.Beatmaps.Formats private void handleEvents(TextWriter writer) { + writer.WriteLine("[Events]"); + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,{beatmap.BeatmapInfo.Metadata.BackgroundFile}")); writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,{beatmap.BeatmapInfo.Metadata.VideoFile}")); From 51bdb73b913255b0f4f2fd8079c0079fc684680e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:49:47 +0900 Subject: [PATCH 060/189] Don't write empty file names --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8acd8eb17b..136b152eff 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -131,8 +131,11 @@ namespace osu.Game.Beatmaps.Formats { writer.WriteLine("[Events]"); - writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,{beatmap.BeatmapInfo.Metadata.BackgroundFile}")); - writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,{beatmap.BeatmapInfo.Metadata.VideoFile}")); + if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,{beatmap.BeatmapInfo.Metadata.BackgroundFile}")); + + if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.VideoFile)) + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,{beatmap.BeatmapInfo.Metadata.VideoFile}")); foreach (var b in beatmap.Breaks) writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); From 60063eefd237572af3c6905096db837dcc0d7401 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 18:51:05 +0900 Subject: [PATCH 061/189] Fix up background/video events --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 136b152eff..f7a70b0d32 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -132,10 +132,10 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[Events]"); if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) - writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,{beatmap.BeatmapInfo.Metadata.BackgroundFile}")); + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0")); if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.VideoFile)) - writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,{beatmap.BeatmapInfo.Metadata.VideoFile}")); + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,\"{beatmap.BeatmapInfo.Metadata.VideoFile}\",0,0")); foreach (var b in beatmap.Breaks) writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); From 4760307bbb6d30b59f14c78ef8a19e4d0af8b582 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 19:01:15 +0900 Subject: [PATCH 062/189] Don't output the first slider control point --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index f7a70b0d32..479f2765bb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -269,8 +269,11 @@ namespace osu.Game.Beatmaps.Formats break; } - writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); - writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + if (i != 0) + { + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); + writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + } } writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); From d35d34c01b148fb96a4f129764a3b53eb7f4e655 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 19:01:43 +0900 Subject: [PATCH 063/189] Fix hanging semicolon for per-node bank output --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 479f2765bb..41cb49615c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -331,10 +331,11 @@ namespace osu.Game.Beatmaps.Formats StringBuilder sb = new StringBuilder(); sb.Append(FormattableString.Invariant($"{(int)normalBank}:")); - sb.Append(FormattableString.Invariant($"{(int)addBank}:")); + sb.Append(FormattableString.Invariant($"{(int)addBank}")); if (!banksOnly) { + sb.Append(":"); sb.Append(FormattableString.Invariant($"{customSampleBank}:")); sb.Append(FormattableString.Invariant($"{volume}:")); sb.Append(FormattableString.Invariant($"{sampleFilename}")); From dedae69db8b4144586eb94f9661c42354749ee32 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 19:52:15 +0900 Subject: [PATCH 064/189] Prefer legacy curve format if possible --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 41cb49615c..f64d505df9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -246,27 +246,42 @@ namespace osu.Game.Beatmaps.Formats if (hitObject is IHasCurve curveData) { + PathType? lastType = null; + for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) { PathControlPoint point = curveData.Path.ControlPoints[i]; - switch (point.Type.Value) + if (point.Type.Value != null) { - case PathType.Bezier: - writer.Write("B|"); - break; + if (point.Type.Value != lastType) + { + switch (point.Type.Value) + { + case PathType.Bezier: + writer.Write("B|"); + break; - case PathType.Catmull: - writer.Write("C|"); - break; + case PathType.Catmull: + writer.Write("C|"); + break; - case PathType.PerfectCurve: - writer.Write("P|"); - break; + case PathType.PerfectCurve: + writer.Write("P|"); + break; - case PathType.Linear: - writer.Write("L|"); - break; + case PathType.Linear: + writer.Write("L|"); + break; + } + + lastType = point.Type.Value; + } + else + { + // New segment with the same type - duplicate the control point + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|")); + } } if (i != 0) From ac984423bb9c149fab9d09922d79e03bd0765418 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 19:53:30 +0900 Subject: [PATCH 065/189] Fix only single sound type being written --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index f64d505df9..0eeacf733d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -225,7 +225,6 @@ namespace osu.Game.Beatmaps.Formats LegacyHitObjectType hitObjectType = (LegacyHitObjectType)(comboData.ComboOffset << 4); if (comboData.NewCombo) hitObjectType |= LegacyHitObjectType.NewCombo; - if (hitObject is IHasCurve _) hitObjectType |= LegacyHitObjectType.Slider; else if (hitObject is IHasEndTime _) @@ -233,16 +232,11 @@ namespace osu.Game.Beatmaps.Formats else hitObjectType |= LegacyHitObjectType.Circle; - LegacyHitSoundType soundType = LegacyHitSoundType.Normal; - HitSampleInfo firstAdditionSound = hitObject.Samples.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL); - if (firstAdditionSound != null) - soundType |= toLegacyHitSound(firstAdditionSound.Name); - writer.Write(FormattableString.Invariant($"{positionData.X},")); writer.Write(FormattableString.Invariant($"{positionData.Y},")); writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); writer.Write(FormattableString.Invariant($"{(int)hitObjectType},")); - writer.Write(FormattableString.Invariant($"{(int)soundType},")); + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); if (hitObject is IHasCurve curveData) { @@ -296,12 +290,7 @@ namespace osu.Game.Beatmaps.Formats for (int i = 0; i < curveData.NodeSamples.Count; i++) { - LegacyHitSoundType type = LegacyHitSoundType.None; - - foreach (var sample in curveData.NodeSamples[i]) - type |= toLegacyHitSound(sample.Name); - - writer.Write(FormattableString.Invariant($"{(int)type}")); + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); } @@ -359,25 +348,29 @@ namespace osu.Game.Beatmaps.Formats return sb.ToString(); } - private LegacyHitSoundType toLegacyHitSound(string hitSoundName) + private LegacyHitSoundType toLegacyHitSoundType(IList samples) { - switch (hitSoundName) + LegacyHitSoundType type = LegacyHitSoundType.None; + + foreach (var sample in samples) { - case HitSampleInfo.HIT_NORMAL: - return LegacyHitSoundType.Normal; + switch (sample.Name) + { + case HitSampleInfo.HIT_WHISTLE: + type |= LegacyHitSoundType.Whistle; + break; - case HitSampleInfo.HIT_WHISTLE: - return LegacyHitSoundType.Whistle; + case HitSampleInfo.HIT_FINISH: + type |= LegacyHitSoundType.Finish; + break; - case HitSampleInfo.HIT_FINISH: - return LegacyHitSoundType.Finish; - - case HitSampleInfo.HIT_CLAP: - return LegacyHitSoundType.Clap; - - default: - return LegacyHitSoundType.None; + case HitSampleInfo.HIT_CLAP: + type |= LegacyHitSoundType.Clap; + break; + } } + + return type; } private LegacySampleBank toLegacySampleBank(string sampleBank) From c6cbf0f28a968ce42c5ba3f7731f98da822ac7e6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 12 Dec 2019 16:27:11 +0300 Subject: [PATCH 066/189] Add 1.0 skin to osu! ruleset test --- .../Resources/old-skin/approachcircle.png | Bin 0 -> 4540 bytes .../Resources/old-skin/hit0.png | Bin 0 -> 12904 bytes .../Resources/old-skin/hit100.png | Bin 0 -> 30853 bytes .../Resources/old-skin/hit300.png | Bin 0 -> 33649 bytes .../Resources/old-skin/hit50.png | Bin 0 -> 27832 bytes .../Resources/old-skin/hitcircle.png | Bin 0 -> 3572 bytes .../Resources/old-skin/hitcircleoverlay.png | Bin 0 -> 7113 bytes .../Resources/old-skin/skin.ini | 2 ++ .../SkinnableTestScene.cs | 5 ++++- 9 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit0.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit100.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit50.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircleoverlay.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/approachcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..ff8b02ce800824f510e8655a1a9d977418bbdeb3 GIT binary patch literal 4540 zcmV;t5ku~YP);J

@NHN+%xmd&%Mk3yL--e&pG%0_n}mDQ`*|v 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�cZa_2kS}f04>ihEu+(-}$68mAH8q>c4Yg}Zw1U5cyjPrzcc;21 zO}$F%L)_@T&0)*iXf-CvhjIO&mqFgsMtO0u9|8C^bCA~uMy{agX#U1Lf3yS=T8V3V zC`|-zcm6mMrruab(wUEJ^lq~9wIU%F(T@Dr`RSvfIRR#VjIn%mnC8%Gu%2`$9~bp< z+maNMH~22(3vM`d=S(0OZ@g$m<3Y_Ge4+FUjoH2rY^OlFHiUDH9u2-Jr!WDfO;0sQ zSH5~hJhf0|pdF`2)~VAW?&z%MnQ=!f&<|VfzMBwInyt$Rx%>X<=SlD4!;UNT>@PlNtt*&nR zy0BzmIP`)3j5bWkjyClEQIg)5FK C#z4*13%nv%sar3_~4ci)7wz%BiUeMi~U~D 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 Dvd!~3 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hit300.png new file mode 100644 index 0000000000000000000000000000000000000000..24945f7d925ee3cd4dbb741907aa1a4e8e90a1c5 GIT binary patch literal 33649 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 PUkRuy>nK&jZ6p5&sNh6p literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/hitcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a3545abf2510e7eec395bf47b73d9082e1fd8d GIT binary patch literal 3572 zcmVGYg2iS*5CRL97HtwqTAM#o zq-m`sA!%);yRO>ZbzK|VrBaKS)!lwq-ZjV5bMJdI^X{8BGv~nNz8PiTo%?;~JLmp* z?-u6f=9<(R*P68plmKc<05v6mni4=w381C~P*Vb^DFM_P!&>V3$6dU5vAa+xEXQRJ z>W5H@=KH8#$Nu~9+}zwXb75vjQT~d{QIvBX9Ua5<+DCqQ<|HbAH-K{ zas7b^@FrW^YnMgf-?=9BJE%uzXJ=nWeWJ6o^NbRp$~HMUxdDI<0Ps(v?y(@_a}x*t zt{?3n0>J%0GVq#|Y5f0JQ6GUq98&^h2fC5-&jaMAP%pE9Q?NM!Bof3K?H7A(2tvtV zKcig5^$_Y8K!&%J0AYadFBXdi^bHhr4!9)uNfdxezoY)~6sZtX*T2w~!u z5}g?*iTB^&H?*z2Bt*9DMpcCj-iLlsza1L4@l{fYO2A2Ed;Mu#2d- z8xl~^IpC7lyObafcqt4>A%T)10-s)AGel@_Z$F>}NS4h%43M8U@!fj70h&v`5BO36 zFNF|}5yD-dGbONP8lhnq)L=*nAlSsjgz@x2fd04yJg+Ihr6zs8e{=PRqyThO!1Dr$ z2q1v@diy|i-xUzl*^1S}j-GVH{N;XuO^ z0I_~Q!2X5-+sJBvg8=xG z68K4gk0U||a{-@(2!aZj`fsQQ5E9;O7y)*S%={y_l$RqFX1c$e2#Q|d#^8*nvX1Tfbx zx3#r>p>7BO@P`2Y6BKlp{(3}DIgBE;R=rB(LKg5h)rm@wtFhzei z4S~!#9+{e&I$F~N0QgVi$9#G6Z0#w-KKIJDc!P z&!+^CK(AG=FEu3aynxLQ+^(b1!ETHW_SFml%p?zhKMBAW_+(#_`+5XFNrEVmz!Jfb z0Oezyot>v@A_SNjJ_EjMYS#sRJxG43A;FawSf{abBq)dBSW9zH0K}{VW_ni|>cxTI zDCQgDax#!Ue84zXu0z1Y#Ke~%{XaEvpC>e~g`GwN-xm((gpnO08u_n4dyBbfb?%fqn_i-x-O4b;G4XTAd!G=l6eRT;XrN(fQB97iJs*3D*ef452d7# z_3!g0>{gus@cwOR*ebfD>)Xwv;3p3ScD7KOI!2jq!fw@*z)P1dO(F4HBF**l)jgH} z@{&S2L1ae-HX)punOWT3-F>Ah3IOjn&*d#iD*Xz6>G=TfKfZ4Q{XI}M1%UUD!TY;? z-mlW1-q6tQ9)j0X6}W}Z_d*4(XQ}}3{;dGCJI?!c$X8iDAbrnNpjb&RP&NV3h^JlK zxTM5St@5N$dLA+=&t^&hc>iiNV7-+2tM`YWBM?IDJ-}p3W(2?uKNq)&Llq#53P>f& zy~;`k%Sr+#Cns+Y{p@CsdGqFp8LAQ>y@XKOR&E!F$dK^u6hZaTDA$G;BY<21Lmhr0Q}Jpq3ZTMyQf6Ret75z2M?xB0K4hKCr4`8 zfgTfbm#0pE8#iuz#LPCu#d;+}ww~`$fd`WQy~MAJpP89iYS*M&TU+lGGYzcZ=R*-J zK5*1!sS^O!L2m*@xVNq_m~{bL+vHF!9PrWe0k<(Q#a>6>cZ49oBA18TMG4aY zxI5 zk2R}hml&pnhmP0klO+EML4d2Sj8qjMUkYKOVh94fr`=mK_TLam;IwN6Q1=B^FXMMC z4_^tP0GEAIuVJ7nQt#^b=@10CqQK`1d>0u|UrwC>_@n2g2Kdg^QOlMHJJ3DXW2qBhc6N5u1-yb@U5^)>GVyWuiAMoz*RK7?@bIuX-D+{%zCaxbWJ3n2 zzatWyNu20CpT@GxvCNbG}ro zDiB8g=_;mUFTfv4`MagM>l>VfIvrd+LP#fuvQGQ1vA z0nAN)?K?$Vi4fM?UEf957ZCqPLla=gAw7yzMn-odu_0i|Cy$o(ikOj+ksB>7Ev@!k z0Dj`PE)J!i{P|8EC1(lru3EJ!tO9Tqpo~-%2tNVjQeS*NSt|Gy6Tq_2Gb5#>NC|i; zzhbIzsGNJ0MF24E1a5f2H5Bpv0JZWHKDz!H`BwWxK0w$e4upc=P!sr zx!l{^+m^3PB^JJY&#Kz3S1d(qmjS|KHum`PYy@jkHqhCt_gr~>6mG#4Fy~Q_;hijkst~9 zH0dK{Hx%|%+U+TX>U4|>zJ<}iUVfM)e-Tt8y?(Y7@adUAsDFX1|M3uQDund9k&%(Z zAi=<fzo|J=EAAHVvQTK;zUm=UQ3ljViAb-Xx0e?ZH>zv_Q4+*|Jhvq`u z^CRj1J<1k1(NxGbX7U^te$b=%VXxSkES?bhlEYd|5aV|7@L@7P9zFuUKQ#E6DF9J| zqww~}=zE0uZepGn)RG#+4RNJs`zR42GQ5U*0N}ryMVkv*dlp6qLkI&;(2;=@3TlZ6 z(mqJn7EN0i`YD8N<>rEg@__ChtJb#!#poIL&$ahp)HHi=hyvyq;@MMbj zD6;)6S^tJYwPGujU?Y^^MF75-zE++qfJ+TrBZbgp@PY0E-X?uK=ONO+3*hgs1`dU4 zCRZRqFMuBez@MUX16&O(u+l-5J_4jghPcI`8f6eY08^6!MT= z^N>Ifop4PU`NBY&B=9}w;M=vEAmMun*Iz-h_pfU9eIYNyD-#j`_zM8L*OeSn1)v8e zCIP)vBE)TNcTM2%F}{%ST}8bUUO$wFelO&0e2t;vQy{`#0M^PMHZ5h8-~v332yruZ zzTurPlgAf#p27bc1o+qT(%*$VPOv}?76D)r5}uTvI_3*WuG~NxF_bf*fVx{}8~xmDfHl z)WQ@i6k#ENe$uD{04!2Ppunf41W7zUW>xPn>hA;i;k@^4q1HBGKotg}(6BkllCp25 zF^K}Wyk5xk|7af3Mmd5QKV2*RS*X=rI8X%>9<~@V;N$wFDp2W!50m=~CaFXHD!lzn zE%sxf9=2gcWH5@b2?W@LuU5vry;M4Om76fcn?n5?^Cm9TC*bL$wcdAydfbNxLKFe; zI$SPC-HX!0j#O_)X`?S)g#Fu$LJ1+vyOGUHFwWxRpHL6yY51lDP)o)1#lA`aH6?(W u5 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini new file mode 100644 index 0000000000..5369de24e9 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini @@ -0,0 +1,2 @@ +[General] +Version: 1.0 \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs index 38aac50df6..2fad3bd04f 100644 --- a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs @@ -19,9 +19,10 @@ namespace osu.Game.Rulesets.Osu.Tests private Skin metricsSkin; private Skin defaultSkin; private Skin specialSkin; + private Skin oldSkin; protected SkinnableTestScene() - : base(2, 2) + : base(2, 3) { } @@ -33,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests metricsSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info); specialSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true); + oldSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true); } public void SetContents(Func creationFunction) @@ -41,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests Cell(1).Child = createProvider(metricsSkin, creationFunction); Cell(2).Child = createProvider(defaultSkin, creationFunction); Cell(3).Child = createProvider(specialSkin, creationFunction); + Cell(4).Child = createProvider(oldSkin, creationFunction); } private Drawable createProvider(Skin skin, Func creationFunction) From ec5b50696058f293dda54c2f0e8b8f2cf9620a57 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 12 Dec 2019 23:41:46 +0800 Subject: [PATCH 067/189] apply mod difficulty settings during song select --- .../SongSelect/TestSceneBeatmapDetailArea.cs | 69 ++++++++++++++++- .../Screens/Select/Details/AdvancedStats.cs | 77 +++++++++++++++---- 2 files changed, 128 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index ed9e01a67e..ad7ef558c4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -8,6 +8,9 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps; using osuTK; @@ -20,8 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect { public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; + private ModDisplay modDisplay; + [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load(OsuGameBase game, RulesetStore rulesets) { BeatmapDetailArea detailsArea; Add(detailsArea = new BeatmapDetailArea @@ -31,6 +36,16 @@ namespace osu.Game.Tests.Visual.SongSelect Size = new Vector2(550f, 450f), }); + Add(modDisplay = new ModDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Position = new Vector2(0, 25), + }); + + modDisplay.Current.BindTo(Mods); + AddStep("all metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { BeatmapInfo = @@ -163,6 +178,58 @@ namespace osu.Game.Tests.Visual.SongSelect })); AddStep("null beatmap", () => detailsArea.Beatmap = null); + + AddStep("with EZ mod", () => + { + detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = + { + Version = "Has Easy Mod", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has the easy mod enabled", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3, + DrainRate = 3, + OverallDifficulty = 3, + ApproachRate = 3, + }, + StarDifficulty = 1f, + } + }); + + Mods.Value = new[] { rulesets.AvailableRulesets.First().CreateInstance().GetAllMods().First(m => m is ModEasy) }; + }); + + AddStep("with HR mod", () => + { + detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = + { + Version = "Has Hard Rock Mod", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has the hard rock mod enabled", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3, + DrainRate = 3, + OverallDifficulty = 3, + ApproachRate = 3, + }, + StarDifficulty = 1f, + } + }); + + Mods.Value = new[] { rulesets.AvailableRulesets.First().CreateInstance().GetAllMods().First(m => m is ModHardRock) }; + }); } } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index c5bdc230d0..be03b7ddd5 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -12,11 +12,21 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using System; using osu.Game.Beatmaps; +using osu.Framework.Bindables; +using System.Collections.Generic; +using osu.Game.Rulesets.Mods; +using System.Linq; namespace osu.Game.Screens.Select.Details { public class AdvancedStats : Container { + [Resolved] + private IBindable> mods { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + private readonly StatisticRow firstValue, hpDrain, accuracy, approachRate, starDifficulty; private BeatmapInfo beatmap; @@ -30,22 +40,7 @@ namespace osu.Game.Screens.Select.Details beatmap = value; - //mania specific - if ((Beatmap?.Ruleset?.ID ?? 0) == 3) - { - firstValue.Title = "Key Amount"; - firstValue.Value = (int)MathF.Round(Beatmap?.BaseDifficulty?.CircleSize ?? 0); - } - else - { - firstValue.Title = "Circle Size"; - firstValue.Value = Beatmap?.BaseDifficulty?.CircleSize ?? 0; - } - - hpDrain.Value = Beatmap?.BaseDifficulty?.DrainRate ?? 0; - accuracy.Value = Beatmap?.BaseDifficulty?.OverallDifficulty ?? 0; - approachRate.Value = Beatmap?.BaseDifficulty?.ApproachRate ?? 0; - starDifficulty.Value = (float)(Beatmap?.StarDifficulty ?? 0); + updateStatistics(); } } @@ -65,12 +60,60 @@ namespace osu.Game.Screens.Select.Details starDifficulty = new StatisticRow(10, true) { Title = "Star Difficulty" }, }, }; + } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { starDifficulty.AccentColour = colours.Yellow; + mods.ValueChanged += _ => updateStatistics(); + } + + private void updateStatistics() + { + BeatmapInfo processed = Beatmap?.Clone(); + + if (processed != null && mods.Value.Any(m => m is IApplicableToDifficulty)) + { + processed.BaseDifficulty = processed.BaseDifficulty.Clone(); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(processed.BaseDifficulty); + } + + BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + BeatmapDifficulty moddedDifficulty = processed?.BaseDifficulty; + + //mania specific + if ((processed?.Ruleset?.ID ?? 0) == 3) + { + firstValue.Title = "Key Amount"; + firstValue.Value = (int)MathF.Round(moddedDifficulty?.CircleSize ?? 0); + } + else + { + firstValue.Title = "Circle Size"; + firstValue.Value = moddedDifficulty?.CircleSize ?? 0; + } + + hpDrain.Value = moddedDifficulty?.DrainRate ?? 0; + accuracy.Value = moddedDifficulty?.OverallDifficulty ?? 0; + approachRate.Value = moddedDifficulty?.ApproachRate ?? 0; + starDifficulty.Value = (float)(processed?.StarDifficulty ?? 0); + + hpDrain.AccentColour = (moddedDifficulty?.DrainRate ?? 0) == (baseDifficulty?.DrainRate ?? 0) ? + Color4.White : (moddedDifficulty?.DrainRate ?? 0) < (baseDifficulty?.DrainRate ?? 0) ? + colours.BlueLight : colours.RedLight; + accuracy.AccentColour = (moddedDifficulty?.OverallDifficulty ?? 0) == (baseDifficulty?.OverallDifficulty ?? 0) ? + Color4.White : (moddedDifficulty?.OverallDifficulty ?? 0) < (baseDifficulty?.OverallDifficulty ?? 0) ? + colours.BlueLight : colours.RedLight; + approachRate.AccentColour = (moddedDifficulty?.ApproachRate ?? 0) == (baseDifficulty?.ApproachRate ?? 0) ? + Color4.White : (moddedDifficulty?.ApproachRate ?? 0) < (baseDifficulty?.ApproachRate ?? 0) ? + colours.BlueLight : colours.RedLight; + firstValue.AccentColour = (moddedDifficulty?.CircleSize ?? 0) == (baseDifficulty?.CircleSize ?? 0) ? + Color4.White : (moddedDifficulty?.CircleSize ?? 0) < (baseDifficulty?.CircleSize ?? 0) ? + colours.BlueLight : colours.RedLight; } private class StatisticRow : Container, IHasAccentColour From 3945e7403aec4c08a249a9fd6a041f71327e96ee Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 13 Dec 2019 09:39:54 +0800 Subject: [PATCH 068/189] improve tests and refactor AdvancedStats --- .../SongSelect/TestSceneBeatmapDetailArea.cs | 6 +- .../Screens/Select/Details/AdvancedStats.cs | 77 ++++++++++++------- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index ad7ef558c4..d4805a73e4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -179,6 +179,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("null beatmap", () => detailsArea.Beatmap = null); + Ruleset ruleset = rulesets.AvailableRulesets.First().CreateInstance(); + AddStep("with EZ mod", () => { detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap @@ -202,7 +204,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - Mods.Value = new[] { rulesets.AvailableRulesets.First().CreateInstance().GetAllMods().First(m => m is ModEasy) }; + Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }; }); AddStep("with HR mod", () => @@ -228,7 +230,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - Mods.Value = new[] { rulesets.AvailableRulesets.First().CreateInstance().GetAllMods().First(m => m is ModHardRock) }; + Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }; }); } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index be03b7ddd5..3e7d4e244b 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -24,9 +24,6 @@ namespace osu.Game.Screens.Select.Details [Resolved] private IBindable> mods { get; set; } - [Resolved] - private OsuColour colours { get; set; } - private readonly StatisticRow firstValue, hpDrain, accuracy, approachRate, starDifficulty; private BeatmapInfo beatmap; @@ -60,11 +57,10 @@ namespace osu.Game.Screens.Select.Details starDifficulty = new StatisticRow(10, true) { Title = "Star Difficulty" }, }, }; - } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { starDifficulty.AccentColour = colours.Yellow; mods.ValueChanged += _ => updateStatistics(); @@ -89,31 +85,24 @@ namespace osu.Game.Screens.Select.Details if ((processed?.Ruleset?.ID ?? 0) == 3) { firstValue.Title = "Key Amount"; - firstValue.Value = (int)MathF.Round(moddedDifficulty?.CircleSize ?? 0); + firstValue.BaseValue = (int)MathF.Round(baseDifficulty?.CircleSize ?? 0); + firstValue.ModdedValue = (int)MathF.Round(moddedDifficulty?.CircleSize ?? 0); } else { firstValue.Title = "Circle Size"; - firstValue.Value = moddedDifficulty?.CircleSize ?? 0; + firstValue.BaseValue = baseDifficulty?.CircleSize ?? 0; + firstValue.ModdedValue = moddedDifficulty?.CircleSize ?? 0; } - hpDrain.Value = moddedDifficulty?.DrainRate ?? 0; - accuracy.Value = moddedDifficulty?.OverallDifficulty ?? 0; - approachRate.Value = moddedDifficulty?.ApproachRate ?? 0; - starDifficulty.Value = (float)(processed?.StarDifficulty ?? 0); + hpDrain.BaseValue = baseDifficulty?.DrainRate ?? 0; + accuracy.BaseValue = baseDifficulty?.OverallDifficulty ?? 0; + approachRate.BaseValue = baseDifficulty?.ApproachRate ?? 0; + starDifficulty.BaseValue = (float)(processed?.StarDifficulty ?? 0); - hpDrain.AccentColour = (moddedDifficulty?.DrainRate ?? 0) == (baseDifficulty?.DrainRate ?? 0) ? - Color4.White : (moddedDifficulty?.DrainRate ?? 0) < (baseDifficulty?.DrainRate ?? 0) ? - colours.BlueLight : colours.RedLight; - accuracy.AccentColour = (moddedDifficulty?.OverallDifficulty ?? 0) == (baseDifficulty?.OverallDifficulty ?? 0) ? - Color4.White : (moddedDifficulty?.OverallDifficulty ?? 0) < (baseDifficulty?.OverallDifficulty ?? 0) ? - colours.BlueLight : colours.RedLight; - approachRate.AccentColour = (moddedDifficulty?.ApproachRate ?? 0) == (baseDifficulty?.ApproachRate ?? 0) ? - Color4.White : (moddedDifficulty?.ApproachRate ?? 0) < (baseDifficulty?.ApproachRate ?? 0) ? - colours.BlueLight : colours.RedLight; - firstValue.AccentColour = (moddedDifficulty?.CircleSize ?? 0) == (baseDifficulty?.CircleSize ?? 0) ? - Color4.White : (moddedDifficulty?.CircleSize ?? 0) < (baseDifficulty?.CircleSize ?? 0) ? - colours.BlueLight : colours.RedLight; + hpDrain.ModdedValue = moddedDifficulty?.DrainRate ?? 0; + accuracy.ModdedValue = moddedDifficulty?.OverallDifficulty ?? 0; + approachRate.ModdedValue = moddedDifficulty?.ApproachRate ?? 0; } private class StatisticRow : Container, IHasAccentColour @@ -124,7 +113,10 @@ namespace osu.Game.Screens.Select.Details private readonly float maxValue; private readonly bool forceDecimalPlaces; private readonly OsuSpriteText name, value; - private readonly Bar bar; + private readonly Bar bar, modBar; + + [Resolved] + private OsuColour colours { get; set; } public string Title { @@ -132,19 +124,39 @@ namespace osu.Game.Screens.Select.Details set => name.Text = value; } - private float difficultyValue; + private float baseValue; - public float Value + private float moddedValue; + + public float BaseValue { - get => difficultyValue; + get => baseValue; set { - difficultyValue = value; + baseValue = value; bar.Length = value / maxValue; this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); } } + public float ModdedValue + { + get => moddedValue; + set + { + moddedValue = value; + modBar.Length = value / maxValue; + this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); + + if (moddedValue > baseValue) + modBar.AccentColour = this.value.Colour = colours.Red; + else if (moddedValue < baseValue) + modBar.AccentColour = this.value.Colour = colours.BlueDark; + else + modBar.AccentColour = this.value.Colour = Color4.White; + } + } + public Color4 AccentColour { get => bar.AccentColour; @@ -178,6 +190,15 @@ namespace osu.Game.Screens.Select.Details BackgroundColour = Color4.White.Opacity(0.5f), Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, }, + modBar = new Bar + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Alpha = 0.5f, + Height = 5, + Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, + }, new Container { Anchor = Anchor.TopRight, From 53a665a034cfa95f926c3334c4a8e77bf370a803 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Dec 2019 20:04:46 +0900 Subject: [PATCH 069/189] Fix sound types and banks not being written correctly --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 0eeacf733d..4c8b83820d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -236,6 +236,10 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{positionData.Y},")); writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); writer.Write(FormattableString.Invariant($"{(int)hitObjectType},")); + + if (hitObject is IHasCurve _) + writer.Write(FormattableString.Invariant($"0,")); // A sound type of "none" is written since it's stored per-node + else writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); if (hitObject is IHasCurve curveData) @@ -303,7 +307,11 @@ namespace osu.Game.Beatmaps.Formats else if (hitObject is IHasEndTime endTimeData) writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); - writer.Write(getSampleBank(hitObject.Samples)); + if (hitObject is IHasCurve _) + writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); // A bank of "none" is written since it's stored per-node + else + writer.Write(getSampleBank(hitObject.Samples)); + writer.Write(Environment.NewLine); } @@ -319,14 +327,11 @@ namespace osu.Game.Beatmaps.Formats { } - private string getSampleBank(IList samples, bool banksOnly = false) + private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); - if (addBank == LegacySampleBank.None) - addBank = normalBank; - string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault()?.Suffix); string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; @@ -334,8 +339,8 @@ namespace osu.Game.Beatmaps.Formats StringBuilder sb = new StringBuilder(); - sb.Append(FormattableString.Invariant($"{(int)normalBank}:")); - sb.Append(FormattableString.Invariant($"{(int)addBank}")); + sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:")); + sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}")); if (!banksOnly) { From 0b7c4f252c19578ff4d1da03cb4d6f3962e497fe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 17:01:59 +0900 Subject: [PATCH 070/189] Fix artist being written in place of version --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 4c8b83820d..efb8527d65 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -108,7 +108,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}")); writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}")); writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.AuthorString}")); - writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.Metadata.Artist}")); + writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}")); writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}")); From 97158fce7de679c07393307303d8438f3ea473f6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 17:02:10 +0900 Subject: [PATCH 071/189] Always attach a new combo to spinners --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index efb8527d65..ff5d6ceebe 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -228,7 +228,7 @@ namespace osu.Game.Beatmaps.Formats if (hitObject is IHasCurve _) hitObjectType |= LegacyHitObjectType.Slider; else if (hitObject is IHasEndTime _) - hitObjectType |= LegacyHitObjectType.Spinner; + hitObjectType |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo; else hitObjectType |= LegacyHitObjectType.Circle; From 654499d8b017c19a37b76ef1a3dbbe45a410ace3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 17:33:18 +0900 Subject: [PATCH 072/189] Remove whitespace --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 1c2ca4b6f8..5bc7897499 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -148,10 +148,6 @@ namespace osu.Game.Beatmaps.Formats Fonts } - - - - internal class LegacyDifficultyControlPoint : DifficultyControlPoint { public LegacyDifficultyControlPoint() From 87a7f340e3100b7f1ee1f9a2c4b05abdb194b86b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 18:12:44 +0900 Subject: [PATCH 073/189] Fix first sample point being treated as redundant --- osu.Game.Tests/NonVisual/ControlPointInfoTest.cs | 12 ++++++------ osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index a51b90851c..0535473a3f 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -60,18 +60,18 @@ namespace osu.Game.Tests.NonVisual { var cpi = new ControlPointInfo(); - cpi.Add(0, new SampleControlPoint()); // is redundant + cpi.Add(0, new SampleControlPoint()); // is *not* redundant, special exception for first sample point cpi.Add(1000, new SampleControlPoint()); // is redundant - Assert.That(cpi.Groups.Count, Is.EqualTo(0)); + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); - Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); cpi.Add(1000, new SampleControlPoint { SampleVolume = 50 }); // is not redundant - Assert.That(cpi.Groups.Count, Is.EqualTo(1)); - Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1)); - Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + Assert.That(cpi.Groups.Count, Is.EqualTo(2)); + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(2)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); } [Test] diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index ce2783004c..6965544292 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -239,7 +239,7 @@ namespace osu.Game.Beatmaps.ControlPoints break; case SampleControlPoint _: - existing = SamplePointAt(time); + existing = binarySearch(SamplePoints, time); break; case DifficultyControlPoint _: From 36f541d8e5ce77a846646063ba10903a93690965 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 18:13:24 +0900 Subject: [PATCH 074/189] Fix incorrect asserts --- osu.Game.Tests/NonVisual/ControlPointInfoTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 0535473a3f..2782e902fe 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.NonVisual cpi.Add(1000, new DifficultyControlPoint()); // is redundant Assert.That(cpi.Groups.Count, Is.EqualTo(0)); - Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant @@ -64,7 +64,7 @@ namespace osu.Game.Tests.NonVisual cpi.Add(1000, new SampleControlPoint()); // is redundant Assert.That(cpi.Groups.Count, Is.EqualTo(1)); - Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); cpi.Add(1000, new SampleControlPoint { SampleVolume = 50 }); // is not redundant @@ -83,7 +83,7 @@ namespace osu.Game.Tests.NonVisual cpi.Add(1000, new EffectControlPoint()); // is redundant Assert.That(cpi.Groups.Count, Is.EqualTo(0)); - Assert.That(cpi.TimingPoints.Count, Is.EqualTo(0)); + Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant From c976427206680d9cfbad6b7ac6a463d50c85081f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 19:00:28 +0900 Subject: [PATCH 075/189] Refactor test to be more complete --- .../Formats/LegacyBeatmapEncoderTest.cs | 112 ++---------------- osu.Game.Tests/Resources/TestResources.cs | 6 +- 2 files changed, 16 insertions(+), 102 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index c4a3877c1c..f2b3a16f68 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -1,16 +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 System.IO; using System.Linq; using NUnit.Framework; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; -using osu.Game.Rulesets.Objects.Types; +using osu.Game.IO.Serialization; using osu.Game.Tests.Resources; -using osuTK; namespace osu.Game.Tests.Beatmaps.Formats { @@ -19,101 +18,18 @@ namespace osu.Game.Tests.Beatmaps.Formats { private const string normal = "Soleily - Renatus (Gamu) [Insane].osu"; - [Test] - public void TestDecodeMetadata() + private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); + + [TestCaseSource(nameof(allBeatmaps))] + public void TestDecodeEncodedBeatmap(string name) { - var beatmap = decode(normal); - var meta = beatmap.BeatmapInfo.Metadata; - Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID); - Assert.AreEqual("Soleily", meta.Artist); - Assert.AreEqual("Soleily", meta.ArtistUnicode); - Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); - Assert.AreEqual("Gamu", meta.AuthorString); - Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile); - Assert.AreEqual(164471, meta.PreviewTime); - Assert.AreEqual(string.Empty, meta.Source); - Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags); - Assert.AreEqual("Renatus", meta.Title); - Assert.AreEqual("Renatus", meta.TitleUnicode); + var decoded = decode(normal, out var encoded); + + Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count)); + Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize())); } - [Test] - public void TestDecodeGeneral() - { - var beatmap = decode(normal); - var beatmapInfo = beatmap.BeatmapInfo; - Assert.AreEqual(0, beatmapInfo.AudioLeadIn); - Assert.AreEqual(false, beatmapInfo.Countdown); - Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); - Assert.AreEqual(false, beatmapInfo.SpecialStyle); - Assert.IsTrue(beatmapInfo.RulesetID == 0); - Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); - Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); - } - - [Test] - public void TestDecodeEditor() - { - var beatmap = decode(normal); - var beatmapInfo = beatmap.BeatmapInfo; - - int[] expectedBookmarks = - { - 11505, 22054, 32604, 43153, 53703, 64252, 74802, 85351, - 95901, 106450, 116999, 119637, 130186, 140735, 151285, - 161834, 164471, 175020, 185570, 196119, 206669, 209306 - }; - Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); - for (int i = 0; i < expectedBookmarks.Length; i++) - Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); - Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); - Assert.AreEqual(4, beatmapInfo.BeatDivisor); - Assert.AreEqual(4, beatmapInfo.GridSize); - Assert.AreEqual(2, beatmapInfo.TimelineZoom); - } - - [Test] - public void TestDecodeDifficulty() - { - var beatmap = decode(normal); - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; - Assert.AreEqual(6.5f, difficulty.DrainRate); - Assert.AreEqual(4, difficulty.CircleSize); - Assert.AreEqual(8, difficulty.OverallDifficulty); - Assert.AreEqual(9, difficulty.ApproachRate); - Assert.AreEqual(1.8, difficulty.SliderMultiplier); - Assert.AreEqual(2, difficulty.SliderTickRate); - } - - [Test] - public void TestDecodeHitObjects() - { - var beatmap = decode(normal); - - var curveData = beatmap.HitObjects[0] as IHasCurve; - var positionData = beatmap.HitObjects[0] as IHasPosition; - - Assert.IsNotNull(positionData); - Assert.IsNotNull(curveData); - Assert.AreEqual(new Vector2(192, 168), positionData.Position); - Assert.AreEqual(956, beatmap.HitObjects[0].StartTime); - Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); - - positionData = beatmap.HitObjects[1] as IHasPosition; - - Assert.IsNotNull(positionData); - Assert.AreEqual(new Vector2(304, 56), positionData.Position); - Assert.AreEqual(1285, beatmap.HitObjects[1].StartTime); - Assert.IsTrue(beatmap.HitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); - } - - private Beatmap decode(string filename) - { - decode(filename, out Beatmap jsonDecoded); - return jsonDecoded; - } - - private Beatmap decode(string filename, out Beatmap decoded) + private Beatmap decode(string filename, out Beatmap encoded) { using (var stream = TestResources.OpenResource(filename)) using (var sr = new LineBufferedReader(stream)) @@ -129,11 +45,7 @@ namespace osu.Game.Tests.Beatmaps.Formats ms.Position = 0; - string result = sr2.ReadToEnd(); - - ms.Position = 0; - - decoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2); + encoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2); return legacyDecoded; } } diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 66084a3204..932021afe5 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -9,9 +9,11 @@ namespace osu.Game.Tests.Resources { public static class TestResources { - public static Stream OpenResource(string name) => new DllResourceStore("osu.Game.Tests.dll").GetStream($"Resources/{name}"); + public static DllResourceStore GetStore() => new DllResourceStore("osu.Game.Tests.dll"); - public static Stream GetTestBeatmapStream(bool virtualTrack = false) => new DllResourceStore("osu.Game.Resources.dll").GetStream($"Beatmaps/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); + public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}"); + + public static Stream GetTestBeatmapStream(bool virtualTrack = false) => GetStore().GetStream($"Beatmaps/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); public static string GetTestBeatmapForImport(bool virtualTrack = false) { From 0ad28a9400b821e2d11a8e0c234f2030387e0119 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 19:11:45 +0900 Subject: [PATCH 076/189] Start at version 128 --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index ff5d6ceebe..f367ab3817 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapEncoder { - public const int LATEST_VERSION = 14234; + public const int LATEST_VERSION = 128; private readonly IBeatmap beatmap; From 9bc02f489ee46f098cbea53d78e3cb6c2d001e5b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 19:14:49 +0900 Subject: [PATCH 077/189] Add missing headers --- osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs | 3 +++ osu.Game/Beatmaps/Legacy/LegacyEventType.cs | 3 +++ osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs | 3 +++ osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs | 3 +++ osu.Game/Beatmaps/Legacy/LegacyOrigins.cs | 3 +++ osu.Game/Beatmaps/Legacy/LegacySampleBank.cs | 3 +++ osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs | 3 +++ 7 files changed, 21 insertions(+) diff --git a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs index ce141031fd..5bf80c34d7 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs @@ -1,3 +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; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs index 57b1e6c29f..32a7122978 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + namespace osu.Game.Beatmaps.Legacy { internal enum LegacyEventType diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index 9223f7df31..ec9839b893 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -1,3 +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; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs index 69adbf8f67..d7743565f8 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs @@ -1,3 +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; namespace osu.Game.Beatmaps.Legacy diff --git a/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs index 93c8920761..31f67d6dfd 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + namespace osu.Game.Beatmaps.Legacy { internal enum LegacyOrigins diff --git a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs index 0d54998d15..8cac29cb87 100644 --- a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs +++ b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + namespace osu.Game.Beatmaps.Legacy { internal enum LegacySampleBank diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs index 509f39f830..5237445640 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + namespace osu.Game.Beatmaps.Legacy { internal enum LegacyStoryLayer From ea4eb6b2043e6cc9cb63c20a2c3d8461f276d983 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 21:29:10 +0900 Subject: [PATCH 078/189] CI cleanups --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index f367ab3817..e16411f343 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -225,9 +225,9 @@ namespace osu.Game.Beatmaps.Formats LegacyHitObjectType hitObjectType = (LegacyHitObjectType)(comboData.ComboOffset << 4); if (comboData.NewCombo) hitObjectType |= LegacyHitObjectType.NewCombo; - if (hitObject is IHasCurve _) + if (hitObject is IHasCurve) hitObjectType |= LegacyHitObjectType.Slider; - else if (hitObject is IHasEndTime _) + else if (hitObject is IHasEndTime) hitObjectType |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo; else hitObjectType |= LegacyHitObjectType.Circle; @@ -237,10 +237,9 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); writer.Write(FormattableString.Invariant($"{(int)hitObjectType},")); - if (hitObject is IHasCurve _) - writer.Write(FormattableString.Invariant($"0,")); // A sound type of "none" is written since it's stored per-node - else - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); + writer.Write(hitObject is IHasCurve + ? FormattableString.Invariant($"0,") + : FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); if (hitObject is IHasCurve curveData) { @@ -307,10 +306,9 @@ namespace osu.Game.Beatmaps.Formats else if (hitObject is IHasEndTime endTimeData) writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); - if (hitObject is IHasCurve _) - writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); // A bank of "none" is written since it's stored per-node - else - writer.Write(getSampleBank(hitObject.Samples)); + writer.Write(hitObject is IHasCurve + ? getSampleBank(hitObject.Samples, zeroBanks: true) + : getSampleBank(hitObject.Samples)); writer.Write(Environment.NewLine); } From 0d49bc244e6972990e83a3b21474e8a1a4af8ad9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 21:30:28 +0900 Subject: [PATCH 079/189] Fix test beatmap not being retrievable anymore --- osu.Game.Tests/Resources/TestResources.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 932021afe5..a57405628a 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Resources public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}"); - public static Stream GetTestBeatmapStream(bool virtualTrack = false) => GetStore().GetStream($"Beatmaps/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); + public static Stream GetTestBeatmapStream(bool virtualTrack = false) => new DllResourceStore("osu.Game.Resources.dll").GetStream($"Beatmaps/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); public static string GetTestBeatmapForImport(bool virtualTrack = false) { From e05c9426ed56921fc982c10e9cf5d3be169ebc78 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 13 Dec 2019 18:50:49 +0100 Subject: [PATCH 080/189] Initial implementation of NewsArticleCover class --- .../Visual/Online/TestSceneNewsOverlay.cs | 38 +++++ osu.Game/Overlays/News/NewsArticleCover.cs | 156 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 osu.Game/Overlays/News/NewsArticleCover.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 98f90f2daa..7903709bd6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -1,6 +1,9 @@ // 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.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.News; @@ -19,11 +22,46 @@ namespace osu.Game.Tests.Visual.Online AddStep(@"Show front page", () => news.ShowFrontPage()); AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); + + AddStep(@"Article covers", () => news.LoadAndShowChild(new NewsCoverTest())); } private class TestNewsOverlay : NewsOverlay { public new void LoadAndShowChild(NewsContent content) => base.LoadAndShowChild(content); } + + private class NewsCoverTest : NewsContent + { + public NewsCoverTest() + { + Spacing = new osuTK.Vector2(0, 10); + + var article = new NewsArticleCover.ArticleInfo() + { + Author = "Ephemeral", + CoverURL = "https://assets.ppy.sh/artists/58/header.jpg", + Time = new DateTime(2019, 12, 4), + Title = "New Featured Artist: Kurokotei" + }; + + Children = new Drawable[] + { + new NewsArticleCover(article) + { + Height = 200 + }, + new NewsArticleCover(article) + { + Height = 120 + }, + new NewsArticleCover(article) + { + RelativeSizeAxes = Axes.None, + Size = new osuTK.Vector2(400, 200), + } + }; + } + } } } diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs new file mode 100644 index 0000000000..3274cedbac --- /dev/null +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -0,0 +1,156 @@ +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Overlays.News +{ + public class NewsArticleCover : Container + { + public NewsArticleCover(ArticleInfo info) + { + RelativeSizeAxes = Axes.X; + Masking = true; + CornerRadius = 4; + + NewsBackground bg; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f)) + }, + new DelayedLoadWrapper(bg = new NewsBackground(info.CoverURL) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Alpha = 0 + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.6f)), + Alpha = 1f, + }, + new DateContainer(info.Time) + { + Margin = new MarginPadding() + { + Right = 20, + Top = 20, + } + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding() + { + Left = 25, + Bottom = 50, + }, + Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold), + Text = info.Title, + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding() + { + Left = 25, + Bottom = 30, + }, + Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold), + Text = "by " + info.Author + } + }; + + bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); + } + + //news article cover background + [LongRunningLoad] + private class NewsBackground : Sprite + { + private readonly string url; + + public NewsBackground(string coverUrl) + { + url = coverUrl ?? "Headers/news"; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore store) + { + Texture = store.Get(url); + } + } + + //date container + private class DateContainer : Container, IHasTooltip + { + private readonly DateTime date; + + public DateContainer(DateTime date) + { + this.date = date; + + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + Masking = true; + CornerRadius = 4; + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), + Text = date.ToString("dd MMM yyy"), + Margin = new MarginPadding() + { + Vertical = 4, + Horizontal = 8, + } + } + }; + } + + public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz"); + } + + //fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now + public class ArticleInfo + { + public string Title { get; set; } + public string CoverURL { get; set; } + public DateTime Time { get; set; } + public string Author { get; set; } + } + } +} From 43720fbf45869a4e22d9ebb1ffebcb7f79e20557 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 13 Dec 2019 18:59:40 +0100 Subject: [PATCH 081/189] Fix CI issues --- .../Visual/Online/TestSceneNewsOverlay.cs | 5 ++--- osu.Game/Overlays/News/NewsArticleCover.cs | 17 ++++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 7903709bd6..f870a12fc3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.News; @@ -37,10 +36,10 @@ namespace osu.Game.Tests.Visual.Online { Spacing = new osuTK.Vector2(0, 10); - var article = new NewsArticleCover.ArticleInfo() + var article = new NewsArticleCover.ArticleInfo { Author = "Ephemeral", - CoverURL = "https://assets.ppy.sh/artists/58/header.jpg", + CoverUrl = "https://assets.ppy.sh/artists/58/header.jpg", Time = new DateTime(2019, 12, 4), Title = "New Featured Artist: Kurokotei" }; diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index 3274cedbac..e2485bd170 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -1,4 +1,7 @@ -using System; +// 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.Color4Extensions; using osu.Framework.Graphics; @@ -31,7 +34,7 @@ namespace osu.Game.Overlays.News RelativeSizeAxes = Axes.Both, Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f)) }, - new DelayedLoadWrapper(bg = new NewsBackground(info.CoverURL) + new DelayedLoadWrapper(bg = new NewsBackground(info.CoverUrl) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -52,7 +55,7 @@ namespace osu.Game.Overlays.News }, new DateContainer(info.Time) { - Margin = new MarginPadding() + Margin = new MarginPadding { Right = 20, Top = 20, @@ -62,7 +65,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Margin = new MarginPadding() + Margin = new MarginPadding { Left = 25, Bottom = 50, @@ -74,7 +77,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Margin = new MarginPadding() + Margin = new MarginPadding { Left = 25, Bottom = 30, @@ -132,7 +135,7 @@ namespace osu.Game.Overlays.News Origin = Anchor.Centre, Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), Text = date.ToString("dd MMM yyy"), - Margin = new MarginPadding() + Margin = new MarginPadding { Vertical = 4, Horizontal = 8, @@ -148,7 +151,7 @@ namespace osu.Game.Overlays.News public class ArticleInfo { public string Title { get; set; } - public string CoverURL { get; set; } + public string CoverUrl { get; set; } public DateTime Time { get; set; } public string Author { get; set; } } From a04f4b76bb93f9a94d06724cf803ac6a0b835a43 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 13 Dec 2019 22:27:14 -0800 Subject: [PATCH 082/189] Allow changing volume using alt when hovering scroll containers --- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index 2721ce55dc..df4c3a3324 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -83,6 +83,13 @@ namespace osu.Game.Graphics.Containers return base.OnDragEnd(e); } + protected override bool OnScroll(ScrollEvent e) + { + if (e.AltPressed) return false; + + return base.OnScroll(e); + } + protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction); protected class OsuScrollbar : ScrollbarContainer From 5af363c92043d81c86d5254e07ac986c06a4d5ad Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 14 Dec 2019 12:58:13 -0800 Subject: [PATCH 083/189] Use default placeholder text on chat channel search box --- osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index 505d2d6f89..25a9a51638 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -115,11 +115,7 @@ namespace osu.Game.Overlays.Chat.Selection Font = OsuFont.GetFont(size: 20), Shadow = false, }, - search = new HeaderSearchTextBox - { - RelativeSizeAxes = Axes.X, - PlaceholderText = @"Search", - }, + search = new HeaderSearchTextBox { RelativeSizeAxes = Axes.X }, }, }, }, From 01c036b0b0c88421a639d9e3d92c7f6b006b421c Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 15 Dec 2019 16:56:02 +0800 Subject: [PATCH 084/189] implement backing beats for nightcore mods --- .../Mods/CatchModNightcore.cs | 3 +- .../Mods/ManiaModNightcore.cs | 3 +- osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs | 3 +- .../Mods/TaikoModNightcore.cs | 3 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 57 ++++++++++++++++++- 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs index da2edcee44..c07087efaf 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.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. +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModNightcore : ModNightcore + public class CatchModNightcore : ModNightcore { public override double ScoreMultiplier => 1.06; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index 2d94fb6af5..4cc712060c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.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. +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModNightcore : ModNightcore + public class ManiaModNightcore : ModNightcore { public override double ScoreMultiplier => 1; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs index 5668c17792..7780e23a26 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModNightcore : ModNightcore + public class OsuModNightcore : ModNightcore { public override double ScoreMultiplier => 1.12; } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs index e45081b6d6..5377eb1072 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModNightcore : ModNightcore + public class TaikoModNightcore : ModNightcore { public override double ScoreMultiplier => 1.12; } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index c14e02e64d..22d8d4ba66 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -1,15 +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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public abstract class ModNightcore : ModDoubleTime + public abstract class ModNightcore : ModDoubleTime, IApplicableToDrawableRuleset + where TObject : HitObject { public override string Name => "Nightcore"; public override string Acronym => "NC"; @@ -34,5 +47,47 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.Overlays.Add(new NightcoreBeatContainer()); + } + + public class NightcoreBeatContainer : BeatSyncedContainer + { + private SkinnableSound hatSample; + private SkinnableSound clapSample; + private SkinnableSound kickSample; + private SkinnableSound finishSample; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + InternalChildren = new Drawable[] + { + hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")), + clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")), + kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")), + finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")), + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (beatIndex > -1) + { + if (beatIndex % 16 == 0) + finishSample?.Play(); + else if (beatIndex % 2 == 0) + kickSample?.Play(); + else if (beatIndex % 2 == 1) + clapSample?.Play(); + else + hatSample?.Play(); + } + } + } } } From 6da168118e84965914bfec15bfae2794676c4470 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 15 Dec 2019 17:02:29 +0800 Subject: [PATCH 085/189] remove unused usings --- osu.Game/Rulesets/Mods/ModNightcore.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 22d8d4ba66..97d96bbca2 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -1,17 +1,13 @@ // 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 osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Audio; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; From 154bc57c6e62136b49f4560af8614fe5889a7d9e Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 15 Dec 2019 17:46:44 +0800 Subject: [PATCH 086/189] remove unused dependency --- osu.Game/Rulesets/Mods/ModNightcore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 97d96bbca2..3a67fedd4d 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods private SkinnableSound finishSample; [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { InternalChildren = new Drawable[] { From 0e658790c141bd28eaa7a62d4b41b2f6b0231e5c Mon Sep 17 00:00:00 2001 From: Ganendra Afrasya Date: Sun, 15 Dec 2019 22:42:44 +0700 Subject: [PATCH 087/189] Add function to assert multiplier value instead of hardcoded string --- .../TestSceneFooterButtonMods.cs | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index e8d3475a11..6eb621ca3b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -28,33 +29,46 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestIncrementMultiplier() { - AddStep(@"Add Hidden", () => changeMods(new Mod[] { new OsuModHidden() })); - AddAssert(@"Check Hidden multiplier", () => footerButtonMods.MultiplierText.Text == @"1.06x"); - AddStep(@"Add HardRock", () => changeMods(new Mod[] { new OsuModHidden() })); - AddAssert(@"Check HardRock multiplier", () => footerButtonMods.MultiplierText.Text == @"1.06x"); - AddStep(@"Add DoubleTime", () => changeMods(new Mod[] { new OsuModDoubleTime() })); - AddAssert(@"Check DoubleTime multiplier", () => footerButtonMods.MultiplierText.Text == @"1.12x"); - AddStep(@"Add multiple Mods", () => changeMods(new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHidden() })); - AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text == @"1.26x"); + var hiddenMod = new Mod[] { new OsuModHidden() }; + AddStep(@"Add Hidden", () => changeMods(hiddenMod)); + AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod)); + + var hardRockMod = new Mod[] { new OsuModHardRock() }; + AddStep(@"Add HardRock", () => changeMods(hardRockMod)); + AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod)); + + var doubleTimeMod = new Mod[] { new OsuModDoubleTime() }; + AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); + AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); + + var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; + AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods)); } [Test] public void TestDecrementMultiplier() { - AddStep(@"Add Easy", () => changeMods(new Mod[] { new OsuModEasy() })); - AddAssert(@"Check Easy multiplier", () => footerButtonMods.MultiplierText.Text == @"0.50x"); - AddStep(@"Add NoFail", () => changeMods(new Mod[] { new OsuModNoFail() })); - AddAssert(@"Check NoFail multiplier", () => footerButtonMods.MultiplierText.Text == @"0.50x"); - AddStep(@"Add Multiple Mods", () => changeMods(new Mod[] { new OsuModEasy(), new OsuModNoFail() })); - AddAssert(@"Check multiple mod multiplier", () => footerButtonMods.MultiplierText.Text == @"0.25x"); + var easyMod = new Mod[] { new OsuModEasy() }; + AddStep(@"Add Easy", () => changeMods(easyMod)); + AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod)); + + var noFailMod = new Mod[] { new OsuModNoFail() }; + AddStep(@"Add NoFail", () => changeMods(noFailMod)); + AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod)); + + var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() }; + AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods)); } [Test] public void TestClearMultiplier() { - AddStep(@"Add mods", () => changeMods(new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() })); + var multipleMods = new Mod[] { new OsuModDoubleTime(), new OsuModFlashlight() }; + AddStep(@"Add mods", () => changeMods(multipleMods)); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddAssert(@"Check empty multiplier", () => string.IsNullOrEmpty(footerButtonMods.MultiplierText.Text)); + AddAssert(@"Check empty multiplier", () => assertModsMultiplier(Array.Empty())); } private void changeMods(IReadOnlyList mods) @@ -62,6 +76,14 @@ namespace osu.Game.Tests.Visual.UserInterface footerButtonMods.Current.Value = mods; } + private bool assertModsMultiplier(IEnumerable mods) + { + var multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + var expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; + + return expectedValue == footerButtonMods.MultiplierText.Text; + } + private class TestFooterButtonMods : FooterButtonMods { public new OsuSpriteText MultiplierText => base.MultiplierText; From 88d3a1707d0b2d3893e497d4eb319d31a845d2d9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Dec 2019 06:35:18 +0800 Subject: [PATCH 088/189] ensure beatIndex is 0 before starting beats --- osu.Game/Rulesets/Mods/ModNightcore.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 3a67fedd4d..1374c385cc 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -55,6 +55,7 @@ namespace osu.Game.Rulesets.Mods private SkinnableSound clapSample; private SkinnableSound kickSample; private SkinnableSound finishSample; + private bool started; [BackgroundDependencyLoader] private void load() @@ -72,7 +73,10 @@ namespace osu.Game.Rulesets.Mods { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (beatIndex > -1) + if (!started && beatIndex == 0) + started = true; + + if (started && beatIndex > -1) { if (beatIndex % 16 == 0) finishSample?.Play(); From 58e3fb0d0fbd506aa4e2bc8342d503f60a7a6a54 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 16:43:20 +0900 Subject: [PATCH 089/189] Coalesce to -1 --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index e16411f343..4280112967 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -112,7 +112,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}")); - writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? 0}")); + writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? -1}")); } private void handleDifficulty(TextWriter writer) From 596fda3c1fd0f1844842a721453e0980d64fcfa6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 16:57:40 +0900 Subject: [PATCH 090/189] Refactor switch --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 4280112967..efe20e8d34 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -191,26 +191,27 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[HitObjects]"); - foreach (var h in beatmap.HitObjects) + switch (beatmap.BeatmapInfo.RulesetID) { - switch (beatmap.BeatmapInfo.RulesetID) - { - case 0: + case 0: + foreach (var h in beatmap.HitObjects) handleOsuHitObject(writer, h); - break; + break; - case 1: + case 1: + foreach (var h in beatmap.HitObjects) handleTaikoHitObject(writer, h); - break; + break; - case 2: + case 2: + foreach (var h in beatmap.HitObjects) handleCatchHitObject(writer, h); - break; + break; - case 3: + case 3: + foreach (var h in beatmap.HitObjects) handleManiaHitObject(writer, h); - break; - } + break; } } From d56e99865cdc859be192c547e895e2792b03c9e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 16:57:49 +0900 Subject: [PATCH 091/189] Throw not implemented exceptions --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index efe20e8d34..e600dc06b7 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -314,17 +314,11 @@ namespace osu.Game.Beatmaps.Formats writer.Write(Environment.NewLine); } - private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) - { - } + private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - private void handleCatchHitObject(TextWriter writer, HitObject hitObject) - { - } + private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - private void handleManiaHitObject(TextWriter writer, HitObject hitObject) - { - } + private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) { From 98dd1c2590796010060759b17cd1a444ab3f60b2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 17:03:58 +0900 Subject: [PATCH 092/189] Use SingleOrDefault() where possible --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index e600dc06b7..5c8d128d5a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -322,13 +322,13 @@ namespace osu.Game.Beatmaps.Formats private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) { - LegacySampleBank normalBank = toLegacySampleBank(samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); + LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault()?.Suffix); string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; - int volume = samples.First().Volume; + int volume = samples.FirstOrDefault()?.Volume ?? 100; StringBuilder sb = new StringBuilder(); From 9fa6954ac2f139171ec33b5b887b25eb4cb20d14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 17:05:24 +0900 Subject: [PATCH 093/189] Refactor getSampleBank a bit --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 5c8d128d5a..9e643857f1 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -325,11 +325,6 @@ namespace osu.Game.Beatmaps.Formats LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); - string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault()?.Suffix); - string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; - - int volume = samples.FirstOrDefault()?.Volume ?? 100; - StringBuilder sb = new StringBuilder(); sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:")); @@ -337,6 +332,10 @@ namespace osu.Game.Beatmaps.Formats if (!banksOnly) { + string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault()?.Suffix); + string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; + int volume = samples.FirstOrDefault()?.Volume ?? 100; + sb.Append(":"); sb.Append(FormattableString.Invariant($"{customSampleBank}:")); sb.Append(FormattableString.Invariant($"{volume}:")); From 27150d6bbc484c14fd1466e3751c64149c35c53a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 17:06:52 +0900 Subject: [PATCH 094/189] Use char concatenation for performance --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 9e643857f1..65edd356d7 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -63,11 +63,11 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); // Todo: Not all countdown types are supported by lazer yet - writer.WriteLine(FormattableString.Invariant($"Countdown: {(beatmap.BeatmapInfo.Countdown ? "1" : "0")}")); + writer.WriteLine(FormattableString.Invariant($"Countdown: {(beatmap.BeatmapInfo.Countdown ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank(beatmap.ControlPointInfo.SamplePoints[0].SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}")); - writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? "1" : "0")}")); + writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); // if (beatmap.BeatmapInfo.UseSkinSprites) // writer.WriteLine(@"UseSkinSprites: 1"); // if (b.AlwaysShowPlayfield) @@ -81,8 +81,8 @@ namespace osu.Game.Beatmaps.Formats // if (b.CountdownOffset > 0) // writer.WriteLine(@"CountdownOffset: " + b.CountdownOffset.ToString()); if (beatmap.BeatmapInfo.RulesetID == 3) - writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? "1" : "0")}")); - writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? "1" : "0")}")); + writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); + writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); // if (b.SamplesMatchPlaybackRate) // writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); } @@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample.Suffix)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); - writer.Write(FormattableString.Invariant($"{(timingPoint != null ? "1" : "0")},")); + writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},")); writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); writer.Write("\n"); } From 5278236458c82cd6b0a0be6a207dbc3ce689af88 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 17:07:30 +0900 Subject: [PATCH 095/189] Use invariant ToLower() --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 65edd356d7..d55adfa756 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -372,7 +372,7 @@ namespace osu.Game.Beatmaps.Formats private LegacySampleBank toLegacySampleBank(string sampleBank) { - switch (sampleBank?.ToLower()) + switch (sampleBank?.ToLowerInvariant()) { case "normal": return LegacySampleBank.Normal; From 3c9884456f0111afb9e71c3ebeeb0860aaa31fae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 17:08:46 +0900 Subject: [PATCH 096/189] Use writeline --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d55adfa756..139be3ce2f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -180,7 +180,7 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},")); writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); - writer.Write("\n"); + writer.WriteLine(); } } @@ -311,7 +311,7 @@ namespace osu.Game.Beatmaps.Formats ? getSampleBank(hitObject.Samples, zeroBanks: true) : getSampleBank(hitObject.Samples)); - writer.Write(Environment.NewLine); + writer.WriteLine(); } private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); From be7b00cc34e64d66b69550f0496f4c948dc62d5d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 17:12:31 +0900 Subject: [PATCH 097/189] Fix potentially incorrect custom sample bank --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 139be3ce2f..d38ff482ad 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -332,7 +332,7 @@ namespace osu.Game.Beatmaps.Formats if (!banksOnly) { - string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault()?.Suffix); + string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))?.Suffix); string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; int volume = samples.FirstOrDefault()?.Volume ?? 100; From 9de6b62fb136e7b7705858970b3b15c818e353de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 18:24:29 +0900 Subject: [PATCH 098/189] Fix nightcore beat not playing if song doesn't hit beat index 0 --- osu.Game/Rulesets/Mods/ModNightcore.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 1374c385cc..4d11b9de1d 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.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; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -55,7 +56,8 @@ namespace osu.Game.Rulesets.Mods private SkinnableSound clapSample; private SkinnableSound kickSample; private SkinnableSound finishSample; - private bool started; + + private int? firstBeat; [BackgroundDependencyLoader] private void load() @@ -73,10 +75,10 @@ namespace osu.Game.Rulesets.Mods { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!started && beatIndex == 0) - started = true; + if (beatIndex < firstBeat || !firstBeat.HasValue) + firstBeat = Math.Max(0, beatIndex / 16 * 16); - if (started && beatIndex > -1) + if (beatIndex > firstBeat) { if (beatIndex % 16 == 0) finishSample?.Play(); From 3e0fda58ea59e10d6fac5ce75d70afc769478bd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 18:47:06 +0900 Subject: [PATCH 099/189] Play finish in addition to kick, not isolated --- osu.Game/Rulesets/Mods/ModNightcore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 4d11b9de1d..f4599ed88a 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -82,7 +82,8 @@ namespace osu.Game.Rulesets.Mods { if (beatIndex % 16 == 0) finishSample?.Play(); - else if (beatIndex % 2 == 0) + + if (beatIndex % 2 == 0) kickSample?.Play(); else if (beatIndex % 2 == 1) clapSample?.Play(); From c883c97bab8e81a3bd67db0ef5d4c7110b5d3161 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 18:50:58 +0900 Subject: [PATCH 100/189] Fix off-by-one starting bar --- osu.Game/Rulesets/Mods/ModNightcore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index f4599ed88a..1c46b82f79 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Mods base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); if (beatIndex < firstBeat || !firstBeat.HasValue) - firstBeat = Math.Max(0, beatIndex / 16 * 16); + firstBeat = Math.Max(0, (beatIndex / 16 + 1) * 16); if (beatIndex > firstBeat) { From 0f9ff32cdc8ee2528024e2c8a3dbf545af5916d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 18:51:22 +0900 Subject: [PATCH 101/189] Fix beat playing while paused --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 8 ++++++++ osu.Game/Rulesets/Mods/ModNightcore.cs | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 2e76ab964f..1374b34aca 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -42,6 +42,8 @@ namespace osu.Game.Graphics.Containers private EffectControlPoint defaultEffect; private TrackAmplitudes defaultAmplitudes; + protected bool IsBeatSyncedWithTrack { get; private set; } + protected override void Update() { Track track = null; @@ -65,10 +67,16 @@ namespace osu.Game.Graphics.Containers effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); if (timingPoint.BeatLength == 0) + { + IsBeatSyncedWithTrack = false; return; + } + + IsBeatSyncedWithTrack = true; } else { + IsBeatSyncedWithTrack = false; currentTrackTime = Clock.CurrentTime; timingPoint = defaultTiming; effectPoint = defaultEffect; diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 1c46b82f79..f1f25f70e6 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -75,7 +75,13 @@ namespace osu.Game.Rulesets.Mods { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (beatIndex < firstBeat || !firstBeat.HasValue) + if (!IsBeatSyncedWithTrack) + { + firstBeat = null; + return; + } + + if (!firstBeat.HasValue || beatIndex < firstBeat) firstBeat = Math.Max(0, (beatIndex / 16 + 1) * 16); if (beatIndex > firstBeat) From 210fecc95188b72cfc1d988fc48936872161ccf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 18:57:11 +0900 Subject: [PATCH 102/189] Fix incorrect hat logic; add support for first barline omission --- osu.Game/Rulesets/Mods/ModNightcore.cs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index f1f25f70e6..830cb87822 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -84,17 +84,25 @@ namespace osu.Game.Rulesets.Mods if (!firstBeat.HasValue || beatIndex < firstBeat) firstBeat = Math.Max(0, (beatIndex / 16 + 1) * 16); - if (beatIndex > firstBeat) + if (beatIndex >= firstBeat) { - if (beatIndex % 16 == 0) + if (beatIndex % 16 == 0 && (beatIndex > firstBeat || !effectPoint.OmitFirstBarLine)) finishSample?.Play(); - if (beatIndex % 2 == 0) - kickSample?.Play(); - else if (beatIndex % 2 == 1) - clapSample?.Play(); - else - hatSample?.Play(); + switch (beatIndex % (int)timingPoint.TimeSignature) + { + case 0: + kickSample?.Play(); + break; + + case 2: + clapSample?.Play(); + break; + + default: + hatSample?.Play(); + break; + } } } } From 72404bff9a7d24466bdc5ef761b44de981f119b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 19:16:54 +0900 Subject: [PATCH 103/189] Add support for simple triple time --- .../Containers/BeatSyncedContainer.cs | 15 +++-- osu.Game/Rulesets/Mods/ModNightcore.cs | 55 +++++++++++++++---- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 1374b34aca..a68e536a18 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -33,6 +33,11 @@ namespace osu.Game.Graphics.Containers /// public double TimeSinceLastBeat { get; private set; } + ///

+ /// How many baets per beatlength to trigger. Defaults to 1. + /// + public int Divisor { get; set; } = 1; + /// /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. /// @@ -82,17 +87,19 @@ namespace osu.Game.Graphics.Containers effectPoint = defaultEffect; } - int beatIndex = (int)((currentTrackTime - timingPoint.Time) / timingPoint.BeatLength); + double beatLength = timingPoint.BeatLength / Divisor; + + int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength); // The beats before the start of the first control point are off by 1, this should do the trick if (currentTrackTime < timingPoint.Time) beatIndex--; - TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % timingPoint.BeatLength; + TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; if (TimeUntilNextBeat < 0) - TimeUntilNextBeat += timingPoint.BeatLength; + TimeUntilNextBeat += beatLength; - TimeSinceLastBeat = timingPoint.BeatLength - TimeUntilNextBeat; + TimeSinceLastBeat = beatLength - TimeUntilNextBeat; if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat) return; diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 830cb87822..16ed3a5e31 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; @@ -59,6 +60,11 @@ namespace osu.Game.Rulesets.Mods private int? firstBeat; + public NightcoreBeatContainer() + { + Divisor = 2; + } + [BackgroundDependencyLoader] private void load() { @@ -71,10 +77,15 @@ namespace osu.Game.Rulesets.Mods }; } + private const int segment_bar_length = 4; + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + int beatsPerBar = (int)timingPoint.TimeSignature; + int segmentLength = beatsPerBar * Divisor * segment_bar_length; + if (!IsBeatSyncedWithTrack) { firstBeat = null; @@ -82,25 +93,49 @@ namespace osu.Game.Rulesets.Mods } if (!firstBeat.HasValue || beatIndex < firstBeat) - firstBeat = Math.Max(0, (beatIndex / 16 + 1) * 16); + firstBeat = Math.Max(0, (beatIndex / segmentLength + 1) * segmentLength); if (beatIndex >= firstBeat) { - if (beatIndex % 16 == 0 && (beatIndex > firstBeat || !effectPoint.OmitFirstBarLine)) + if (beatIndex % segmentLength == 0 && (beatIndex > firstBeat || !effectPoint.OmitFirstBarLine)) finishSample?.Play(); - switch (beatIndex % (int)timingPoint.TimeSignature) + switch (timingPoint.TimeSignature) { - case 0: - kickSample?.Play(); + case TimeSignatures.SimpleTriple: + switch (beatIndex % 6) + { + case 0: + kickSample?.Play(); + break; + + case 3: + clapSample?.Play(); + break; + + default: + hatSample?.Play(); + break; + } + break; - case 2: - clapSample?.Play(); - break; + case TimeSignatures.SimpleQuadruple: + switch (beatIndex % 4) + { + case 0: + kickSample?.Play(); + break; + + case 2: + clapSample?.Play(); + break; + + default: + hatSample?.Play(); + break; + } - default: - hatSample?.Play(); break; } } From cfd811112061663c00edf15096273da32a1f5102 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 19:40:43 +0900 Subject: [PATCH 104/189] Better initial beat handling --- osu.Game/Rulesets/Mods/ModNightcore.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 16ed3a5e31..abbeca3150 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.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 osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -93,7 +92,7 @@ namespace osu.Game.Rulesets.Mods } if (!firstBeat.HasValue || beatIndex < firstBeat) - firstBeat = Math.Max(0, (beatIndex / segmentLength + 1) * segmentLength); + firstBeat = beatIndex < 0 ? 0 : (beatIndex / segmentLength + 1) * segmentLength; if (beatIndex >= firstBeat) { From 7d979e12649ef7ea4346a7c535ebd2ace3202f14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 19:41:04 +0900 Subject: [PATCH 105/189] Add finalised test scene --- .../TestSceneNightcoreBeatContainer.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs new file mode 100644 index 0000000000..3473b03eaf --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -0,0 +1,38 @@ +// 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.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Visual.UserInterface; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneNightcoreBeatContainer : TestSceneBeatSyncedContainer + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ModNightcore<>) + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + + Beatmap.Value.Track.Start(); + Beatmap.Value.Track.Seek(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); + + Add(new ModNightcore.NightcoreBeatContainer()); + + AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleQuadruple)); + AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleTriple)); + } + } +} From 438d97f4f56ac84058745dafd809b5fa9198ab18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2019 19:42:28 +0900 Subject: [PATCH 106/189] Rename variable for clarity --- osu.Game/Rulesets/Mods/ModNightcore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index abbeca3150..16902cc792 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -76,14 +76,14 @@ namespace osu.Game.Rulesets.Mods }; } - private const int segment_bar_length = 4; + private const int bars_per_segment = 4; protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); int beatsPerBar = (int)timingPoint.TimeSignature; - int segmentLength = beatsPerBar * Divisor * segment_bar_length; + int segmentLength = beatsPerBar * Divisor * bars_per_segment; if (!IsBeatSyncedWithTrack) { From 795416c066437f9b5f1cc44898c68c429b2059ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 12:06:09 +0900 Subject: [PATCH 107/189] Move first beat offset to BeatSyncedContainer --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index a68e536a18..b04d01004a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -89,7 +89,7 @@ namespace osu.Game.Graphics.Containers double beatLength = timingPoint.BeatLength / Divisor; - int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength); + int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (effectPoint.OmitFirstBarLine ? 1 : 0); // The beats before the start of the first control point are off by 1, this should do the trick if (currentTrackTime < timingPoint.Time) From 87035f8251965b81d5a5ecb572a6fd803fd97837 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 12:12:15 +0900 Subject: [PATCH 108/189] Simplify complex method --- osu.Game/Rulesets/Mods/ModNightcore.cs | 72 ++++++++++++++------------ 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 16902cc792..a8c79bb896 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -92,51 +92,55 @@ namespace osu.Game.Rulesets.Mods } if (!firstBeat.HasValue || beatIndex < firstBeat) + // decide on a good starting beat index if once has not yet been decided. firstBeat = beatIndex < 0 ? 0 : (beatIndex / segmentLength + 1) * segmentLength; if (beatIndex >= firstBeat) + playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature); + } + + private void playBeatFor(int beatIndex, TimeSignatures signature) + { + if (beatIndex == 0) + finishSample?.Play(); + + switch (signature) { - if (beatIndex % segmentLength == 0 && (beatIndex > firstBeat || !effectPoint.OmitFirstBarLine)) - finishSample?.Play(); + case TimeSignatures.SimpleTriple: + switch (beatIndex % 6) + { + case 0: + kickSample?.Play(); + break; - switch (timingPoint.TimeSignature) - { - case TimeSignatures.SimpleTriple: - switch (beatIndex % 6) - { - case 0: - kickSample?.Play(); - break; + case 3: + clapSample?.Play(); + break; - case 3: - clapSample?.Play(); - break; + default: + hatSample?.Play(); + break; + } - default: - hatSample?.Play(); - break; - } + break; - break; + case TimeSignatures.SimpleQuadruple: + switch (beatIndex % 4) + { + case 0: + kickSample?.Play(); + break; - case TimeSignatures.SimpleQuadruple: - switch (beatIndex % 4) - { - case 0: - kickSample?.Play(); - break; + case 2: + clapSample?.Play(); + break; - case 2: - clapSample?.Play(); - break; + default: + hatSample?.Play(); + break; + } - default: - hatSample?.Play(); - break; - } - - break; - } + break; } } } From a85653ebec2607ca2675a1b106ebcd814c75fa66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 12:24:59 +0900 Subject: [PATCH 109/189] Add comment --- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index df4c3a3324..ab72276ad0 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -85,6 +85,8 @@ namespace osu.Game.Graphics.Containers protected override bool OnScroll(ScrollEvent e) { + // allow for controlling volume when alt is held. + // mostly for compatibility with osu-stable. if (e.AltPressed) return false; return base.OnScroll(e); From f8ffa676931ebdc01946520c5cf5072ac7caf2f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 13:21:23 +0900 Subject: [PATCH 110/189] Add test and isolate ignore bindable from EnableUserDim --- .../Visual/Background/TestSceneUserDimContainer.cs | 14 ++++++++++++++ osu.Game/Graphics/Containers/UserDimContainer.cs | 12 ++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index 472c43096f..d5d4c7e5ec 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -88,6 +88,20 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("not lightened", () => userDimContainer.DimEqual(test_user_dim)); } + [Test] + public void TestIgnoreUserSettings() + { + AddStep("set dim level 0.6", () => userDimContainer.UserDimLevel.Value = test_user_dim); + AddUntilStep("dim reached", () => userDimContainer.DimEqual(test_user_dim)); + + AddStep($"ignore settings", () => userDimContainer.IgnoreUserSettings.Value = true); + AddUntilStep("no dim", () => userDimContainer.DimEqual(0)); + AddStep("set break", () => isBreakTime.Value = true); + AddAssert("no dim", () => userDimContainer.DimEqual(0)); + AddStep("clear break", () => isBreakTime.Value = false); + AddAssert("no dim", () => userDimContainer.DimEqual(0)); + } + private class TestUserDimContainer : UserDimContainer { public bool DimEqual(float expectedDimLevel) => Content.Colour == OsuColour.Gray(1f - expectedDimLevel); diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index e44e7a0d57..65c104b92f 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Graphics.Containers private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0; - protected float DimLevel => Math.Max(EnableUserDim.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0); + protected float DimLevel => Math.Max(EnableUserDim.Value && !IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0); protected override Container Content => dimContent; @@ -88,7 +88,7 @@ namespace osu.Game.Graphics.Containers ShowStoryboard.ValueChanged += _ => UpdateVisuals(); ShowVideo.ValueChanged += _ => UpdateVisuals(); StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); - IgnoreUserSettings.ValueChanged += _ => updateSettings(); + IgnoreUserSettings.ValueChanged += _ => UpdateVisuals(); } protected override void LoadComplete() @@ -112,13 +112,5 @@ namespace osu.Game.Graphics.Containers dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); dimContent.FadeColour(OsuColour.Gray(1f - DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint); } - - /// - /// Invoked when the IgnoreUserSettings bindable is changed - /// - private void updateSettings() - { - EnableUserDim.Value = !IgnoreUserSettings.Value; - } } } From 46dc2251e88703029e0234c18794850a5c1475e9 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 21:39:33 +0800 Subject: [PATCH 111/189] Add fxcop with every violated rule off. --- CodeAnalysis/osu.ruleset | 63 ++++++++++++++++++++++++++++++++++++++++ Directory.Build.props | 4 +++ osu.sln | 1 + 3 files changed, 68 insertions(+) create mode 100644 CodeAnalysis/osu.ruleset diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset new file mode 100644 index 0000000000..9bcca40983 --- /dev/null +++ b/CodeAnalysis/osu.ruleset @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index c0d740bac1..27a0bd0d48 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,11 @@ + + + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset + true $(NoWarn);CS1591 diff --git a/osu.sln b/osu.sln index 1f4faae6b9..79823848f0 100644 --- a/osu.sln +++ b/osu.sln @@ -60,6 +60,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json osu.Android.props = osu.Android.props osu.iOS.props = osu.iOS.props + CodeAnalysis\osu.ruleset = CodeAnalysis\osu.ruleset osu.sln.DotSettings = osu.sln.DotSettings osu.TestProject.props = osu.TestProject.props EndProjectSection From 9875fcea9971470479006fe81bf15103142fdb79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 13:54:25 +0900 Subject: [PATCH 112/189] Add numbers to old skin for better identification --- .../Resources/old-skin/default-0.png | Bin 0 -> 2003 bytes .../Resources/old-skin/default-1.png | Bin 0 -> 1191 bytes .../Resources/old-skin/default-2.png | Bin 0 -> 1756 bytes .../Resources/old-skin/default-3.png | Bin 0 -> 1822 bytes .../Resources/old-skin/default-4.png | Bin 0 -> 1814 bytes .../Resources/old-skin/default-5.png | Bin 0 -> 1848 bytes .../Resources/old-skin/default-6.png | Bin 0 -> 2014 bytes .../Resources/old-skin/default-7.png | Bin 0 -> 1452 bytes .../Resources/old-skin/default-8.png | Bin 0 -> 1953 bytes .../Resources/old-skin/default-9.png | Bin 0 -> 1814 bytes 10 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-1.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-3.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-6.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-8.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-9.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-0.png new file mode 100644 index 0000000000000000000000000000000000000000..2af0569bcb30f4e87135669268698a79ac486f1f GIT binary patch literal 2003 zcmV;^2Q2uBP)0&s!{Eg(K)vw7OGA_klVuXw5~tP0kR@Dz8%i8MTl@(y z-HHy1)1^gl{JB-y+HK0-x1Q6y9t-`>_Z71(JIO;iA0MaZIq(1befqh%Io-c~gz5eR zLm$Gm41gB_aex&76Cer@2?%HRpx7S*9suqGCK+)4NX?g`CPir6_!2+@zzkReSO<_@ z^LM}y;5NVxn2{xn2}vVXR=YR*Dp~j2@G20fY&*JmX?;P$jC^e%tp`?UA%a4 zu&1YI6!hJjnwo0bzI}UET3VV}Mj~dj*+MQgfi!+r62RUDG$O!&o84~r!#T;(-`_t1 zr}!~nhvLwoLto3R)z;S5PyYV{z;cY#9Qpu~Nd?qsv4QUH?i(a@TN=^W5s+ zINU!W^Bs7wM5WSLKIbQq$h?xz7Su@L?Afz4dAr6`C(!syOAst9EQ}-NzzRxYHNR=s zu3gdz9R@X$wA};TU93o+l9ECXIGU4)pvhf!cD7k|;j34#(ya4GsxqM->FMe5LV4gL zVPg8_OE1)@(z$czD2ZQ!RAic(6H!r7MxjXaEf~YS645tVd*DWlMgMq^+n4fRFqaBN zw+4T#B&sA$0s|IFmu&!-QWAoGYhn^jPDVP-76R5i1;za`zg0#@8#V6#F((n_Uwzp! z=|!zp>m;o(75s~)Lz|tQ^^&QVlUPa%5!ppYMn;@M4{82_Uat>l%{8u0(%&~UG(-cW zi<2;Dk!NRT=LCrlP6euz!W#{;MOJ{}4oNST81*z4??Raas~UBiH*Z!|=0c@7N~ZTU zlfV`WRaXT@?cX?%NY*HM2PN^RS`t%JQ`81LLF~c6YZV+DHf%7fkUDkec52vedXomh zJW`-adE6e;ka%ICUKk*YCz)zgskynCR*g47l^%+c7!wjX z#l<_Bu4KVXl)`fOc$3fT>+5sW)GNo19pjlYI-)yb?&RjhJ6b*#`!ZjEM#_GDjmH4N zBdzDEg!iiHq8~kaw2OEp?4V4EB+$Y~_=QuaPDzp2wrv}i1{PPZa%sRPM&k^;h2Lwj zSfo2B!^6W4^4tR5xD{0CQJ?~4lo^Ze#EBCQ{70oA`X}+PQNlZN#-x8x~2pS*Ol7_U_%=En^qhTp*W9bx$4#6z~h!0;;X8EmEUKq=VAO z4EmXP6fPuFsMD)#Hk&IUA>j|u;WbLWtfZu5?bz7ZIC!_G5{GOg3pBG6=N{Y_U&tI!1Q-ydi0I`H>!)LlD z45>cBG@7uSp104&w z{3XU<8rTAj@Oe6P7}95XE9d0NleZwRn%uN$)5`{fAtKZS&YU@Osj{-Nqqnzrh9r|; z00x5*STNrSBoQ>!hK7b5aI3jmt1@(-*mT1twD2$ir-<9c^HomT($(d0h7A8xHu!Y zZ17;uHPe5A_(nb#ulv=dk=v5~f3R1R!9V>)r0sg5V) zH9!!_AOU<)Wi?B)Q+RVX$fobIVg`XPp}3U3NfT-k;)r0a7;M5VXA%n|UdF>(IS6kk lHvJ5V|K9=phm5}k7yxVV^TAT2(qBnTK@ z0-l32%{aNe_uRQn>?9|hf!n#ip2wW8x3=Ts3m|d+4Qb+fVseKf#rdn zA?|+&whi_j>@!#lY)`y^MH#JOM?c(q&ai4^Z*+HeKd=%Lm&=tO3A<6cJILgb*v)S|t*jXcwTurt&j1E-0hBxf&;7YnT zaKvIUo~)Z-yL#n|N9;wGU>Q!QGizaCA(Ck0yu7^Z+}zwOiin+^oxP!S<4u>OyDx{~UhYP_b_|;Gd6SKuUGbbgpp9`t4 zuP>&Gn4O&!z42De57RsXBCzh=d|g#lMHVtPHYN&LW+|WyMwhvehK7cGGhylN?d3wy z(_gF-T0Ak^+uOZn!Xh-gFxXFJArKb)^hJCPozzWzFLZ6OLOwxQ4y||u5%CTW3-#G%EpOKiF?SBr{}0zPSNZ7njxA)!!clPzCqVIo-wgyl56+~n(;ni_Jo1OkD1Vfhil za$v)$cVI>ZjFcm#>+F2@4IJ*tqtzP@=%Sg1mf)-rL@(a}LpQi(m5Sff)D!f5RVU)R;u zk%dU?u>@h+wc?TUV7K|g=kt+;=&^^25GQMNC+uM+ECPG1COS4pH=SMgkj!@Zy;t^zzGL^7M?C~|t z6(P*kfX1LgbBAOApi1 z{yuZhQ9(k1J@9cko>SVso=31tK4U#XWniAXQxWlxN0?XA z85)+b3Nrd#2H(8Hm=weF!_2XsP{{urDf3fW$_V)*zyQwC!s?l~#8?0T002ovPDHLk FV1oRZC2Ifx literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-2.png new file mode 100644 index 0000000000000000000000000000000000000000..dbf7bc73bc620b8782be5518443a36c91d1216cb GIT binary patch literal 1756 zcmV<21|#{2P)p$w|{(9A*RO z%p)u&yhey6EFmmnL=hHhoQDXF6GjOWgfYS};Su3)o_`i<;vgGCSVdS(SWj3U!T>%b z+#z@f_XrR8=7J&~#YA@yR?dK)=2trhXbg7}s#TVi5kV`Gf|W4K6PU!MmH&*(K^ z1P2Zr5N41hELn{>GGIKuq^71W2{vM6m$taLxGxF|3vJM?k?Rx}7t2q5wLguAeAL(1 zd#qNg4NpPuxe7O14mqn4ALo>7ayp%lgY{;GQ%tnBwmOYm3$o>}uS3pi#Q)`b!GuH* zj6OR$I(}8oojrT@wvlT|vPE&XXc}?OPItxe$&)9q8eN!WWo6gZBohMVS+q2cOy!fw z@#DvP+S=NB^j^8Ds_L?ox~~v!hiJJVuurv!|Aove4l*(_?0U#oR8-h;!+eMW>3Xn6 z(A?ZC90_TERP|ykmtMlRGWo-Y51;p^d3g4&UAr13(ik?vbCLCOa&i)k$WE$T{y?p6 z(fTowA0yl8;zM7tRasf-2Jp|bv$I#m#Kicz9gWfyi~K8Y5T<+>=jkQxa=GMn;U#X* zE&fiB$fHBaVk<)R=9P*cwed5djUQ_i*BQP+Ph_KB;!RCWZt3uM-|7r>5|ekK_9v_( zY=AqdoD1*oBftXtQN_`D5gct4Z6>5{y#*U&LWQ0n{+Oi_m^hP%Muz|*gJ@nNyo#*O zt9(Q>yrS6WVhug&PH`Qw$M^L{n|$9P>HC^+JB!{65wWKn-3I}F8i%zg4o;mqWi=A* zW5(j~V$B3@mV+SuA<7HU_T9R5>oZh} zdamPeINa&!>F1@S^Eq)H)NCX|O`7VJJiVlmEB5T!W0$7BferpI=ZA=}nHMYMkuH-q zqU-(p_n(&}f^y*oudUD#XZKi!+(JkWmNd1BHYrCSdtV{aqtmLaAW27XD*uGAG{mHl zXue~cRoxRbO8ONvuY@8&8ty^q|5LrB(b6eMBiWsSi%e>`hsB^Y;&?A{tGvra2`0;k zdtf0-nuc?HjZb;!Dk$Pa8jbW?&W6k6VDJ3N( z88eMCBHY>8={|Dg$W@K~Z!`#M^G`Ta-r+?$?w2@|ewXlp{3FQX=+UFrnQ9ptzHs4! zi*D4Zh_I0X8FeekE2XkZCas4s$Wr^}K7IQ1?Eu}8-tjRfqkd(5ZQ`ge;2%{2y}iBY zV!}4)iTxzF5K;tB0XcKx)!#O;-Sr|~m*^RFb#*T3aKvBA2T?OiWPLbEqpX$W=2Dil zQ^YyJtO=$Q59P4gY|3_RTr1+(P)rdr3CF7w#r~qaS$fxBb%?jbzE>2vxw&E0Hrz?_ z{4U-OoKBo&vQA87W@aXalXzoeqbw7*Rz^j{k#yb`>3p`{Woiv+^Jik3cPofTibjA+ z%&?M1ZLZk-d8k>dFzI}g^bYYQ8DEl2Zu*Nj%pEWL|9Oc>+x^L8s>q@RYT=G%13S#Z yFpz!1`;+&0({xl z!WaUPdgUhj#U&~~aA31EC}4od2qXp);Rk>C$q(V@0L@|`3WhWb5t+!%0Xk{M#jwO7 zFy87s^+8jVj(AW5lU`k2HI|i?>6ues16J%NAXsfYe_EJVR8;t6wX3VE zSrhpVa0^y@&r#+K0;sL=Co@y+3lnK+X)(|g6VEjuz3VLHV>CI12US&7Q6V8Ahka}! zeHcmD@7Rmu)|4I@85uk~J8N*4gi@&t=6&$O!on_*i7c2!nNn^mbApYdV-}Nxx;?_f z!vm(Krrzfv-rn9mnxCI9tTsjy9dP{X5sr3<1W z930H%4Sf)^H{*i zBS{23h>YEV&d$zkj2iBePfAL3r{lM!Xu;d&A9CaVthdcD?N@}gDm z0j^g(N?h~+p(Fv+QpuyO2L%OPpy_qMm(EHzw+0F`z+JRQz&J@&lSuWsJkUqbN^g;mSxM zT!Pmws0%MwZN(QZF1vWPUU$a#S@Iu^!cEWv=#M*<86LjhJejQzO*3&iVy0Oqs=}c^78V|VEb9L^3T!n)9mi?uJ-iwG}{)zAn>o) zRH7&YS{gtoba-xnJ#*CK%&C_sDjlchk}Nkjms_j%_xH~;(|h9TJb<>r(jQz|St)T> zBrz%a0kswq$#J<~3W$==I$n?MulP}{wpWMGgDweGY!)P?#mqK!LbJHXy&Ex1u8VV5|6Z3J^r>Hsg2ytxRT5j0W(_@fAd~9rN zQD_QHOiX0+kjBNaC8iu}0 z)H~d>*;k=$m=R7u93Pg@Ru1KL!s6m$9lpP0NF$dHN2INV1oi*UQajCJo0XN7Y2JE0 zh%4OK*!UCkszew+5Z262DjaJCM6bTjQ<}EeDTJ}Nq|}UJvgiXq_NEQNbqnx4;10eD zbg9~6h$~2Fz03AZPy#@@j*Ezh2z8d6Fe)r8ELfZioYMfu#?jHyKN*J;k2$K8egz&I zTxBA=BUD77D9Ql5?4{6dqA>RhzySB(bgRC#^C|^bHkMMh$^%4ilPQtOJaYUw8a|12={s5$!(MW|36v#JpL1403@th-uUEs@Bjb+ M07*qoM6N<$f~Rj>w*UYD literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-4.png new file mode 100644 index 0000000000000000000000000000000000000000..4564f6d8bff88ce7a1e983aa9192b8c882789f5d GIT binary patch literal 1814 zcmV+x2kH2UP)MX4xD zf25+&_`^?;7>(E#Nd$>eNnaomF(fo5Ch!6eeIN!x0uMwXfe?AmHFJrMt*yQ6a5&_FpC`1^Z%w=C zJCY8WYw)Tc313)1#2v`)6Jb+6$LNigvs5SK0`Y68Roagv#t zd04gI?(XgpsX0WL_w_Uuj!^g^YjkvUm{A7?@uH%lNN(QG(FqU9pAJXFeQ9ZFQL245 zH#f^fMrnrsQX#Tpr%s)UGP@en)6?beh7t+$b>j)X=yP{WO#V^nzYkT ziD-p&VPZL<2?uf!UK@vnZc~*cFyuGx+{z;nBC(An#oOW<1R!anXgv}n{$vuE zg|7&3&&S1m85tRoCf&**?`=v%m;{7fnh2L5k)NNR||qVjAY%u8;Hj*e1)OT4L0@dLYwS=z2sXh`JJ z%nFIRy1F#Irw9_*^);6di-j#4_6{SG#Dy$+QPaLj_>#sv?CXNWD}<+o7IH^|T&d3m z30*Ui&cZ4yD?M$zNSj}U+pZQLhy%XR^MwTqMMWqq>!TaKBZ*Om^nL@*6nY(h( zesPn{W{cm-Bw%fAZ3A+Gg>sOHTKmM+q@<)HmMw0Y1ZzYt2dx$vjA$Tln2QAKu5!?V zeB)LoVz=96w;C2o*jGwvSFF0a`rEBcq`bUb-nYh9_dt$XSRk>S;h zK?z%})-&Avj&PR7@05u^BtAxXhwD!3W*r?JNqV`9E2Ti0XQyJN6UP!^m&@uJd}_D{>E`RYWN-cbhDXLyEsd@;Ht{L zsYE0)D6<~oHKX!BSCcIMuCIl}x)SlW{ZC}8+1~;T0CqcF2-dk}y8r+H07*qoM6N<$ Ef`bE6AOHXW literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-5.png new file mode 100644 index 0000000000000000000000000000000000000000..dcef35eb594a4c047cf37dcfb7acb660cb155de6 GIT binary patch literal 1848 zcmV-82gmq{P)Gbi!bE0Cyd-XtDVqi0*(4Qw9_}$layrtC&D(6YcsjiO9|6~R zw>L+c{Bat|8k`(GdNjAbzP>OVh-YVK=d-f1j(NS_d6;U139Ua(hfQK$R#x_6xYRu` zFyLn3828LXo99Sg!AX98er#%L>bh`&Z)|M*Mw*VB0iP~T4jw$XHC(_)M@KzFLqiXF zdWGaxgrdo;qQjz@*zNYraDi`YYr7;(uTY?wRn`3S?CDx{b#+>Ne0*#;fKN|PKWu4f zar5*x$%ImdC9SWZLzb9VRaI>Xm)4z5r>yk>l837F0HJZw#&`{Vp7Cb z*5)JLiD{BMIO?@(89Y4

(~N*VWY-)_PxG-&H9}Xk54?1+p-D7JTD-EXL?UmePC- z$;)Qwz`1kh+zLv}X2IyCg&;cJrA0grs@!132;BtXu@>hI4GkL)A3nTuxk#3X;oaTc zUW!N_-B(~zXGpGt!zhDiI#5Eg-KUP;-jc$?!jMn`BrxGF6m6|3zDHg3kU{& zEge1rEg&b2x^d%1jnS-XWf4b?9O*r9;J`nk6p4#)&Z)FMl0>OT8FFK&fn(qq`}glJ z!7fXeQ7f9Kivd1=|% z*=bf6v1iYoGZK&QAo*I3K*7MTMnWaYHS=Kj3Cyjww$@>`Xh9K@kdSap6x<}A5Z-%B znhkvk;i{YDEACV&4MYt%UQSNVr$|_4z+=&%-#lHbPja@UnohI>aMaZX+IdjKD(3vD zB(IUI5FeTiwJw*-O=ft8r=5hi$1r$aKqO~dkTg^=2wO9OG9c%O1Qc8(uZ!y*kEc?# zRiZPUetPETOVR@Z5KV^^ZgmI|wtJW4CVdB)@g}G8dCBCks5|ue1v)+!iSFddla~+* zRZT}nR|1DhXqg0{OG!Q?d6VQN2{=X~Ngl`Gk4XxU)JSYPzk#m|YJYWjwf6RQkHr8K z5s;X%c_600r(Y%LaAIPjFHwz*j69Hv3|}hXo#~`|+|ij6qQf}utNV@NtSe3D{DPN& ze;}@*8UMk>!C;8Ez<}=tSeN;d-B4B5*0Ay9ZRK(hk zlSAdMUAvUxJd&H_Cj;z1e}iYX_JX*uckf=iSw`5@)FgL}QFS@klzd82G&vCl0X9MbtVvEzj*W?li3$|c zMJsP?Y;2OE&Ci{kogOleK(BR%*)eY!)ppPtmKU&rTYX6(Fm;4uyxkjR(6eT5JL3I7 mi6*4r|Ig!z{*T$80t^7vQFQ6vL4daa00000P)M@{kOuN$il_TJ-Ot(W@o;#ba}JhU&vxzQeb4)GJaj)z#UFiHWA^uAi8g zxK~qCb86?#ofl~L1%7k0Po9WF*NKF;2@&deT3Xtoy?gg&W@l$7d%AvjczC3wq~z%F zekB?8<4Gf%z z^X2t~Jj4SXExHat-C^yo|Tl57Ht3SXN0opU>ew-y7p}6FH&lTEg#~ z_pA_D&ho@LH#ncic{ajL;x_5gCf$%n$p29J@%HxiBBSn5C0QvcDRm-OfgiuhiCck8 z+-4YO3Fp|bCuek(L_9)<{My>u3?1Sam6erEf_N*hWDchj0Wx{Z&(C+>ySce}P+W(R z&S)ZDKhXq6rX|l26ctYb_DSa^l8jl0IOMW_|9%_q_9Ai;-jGEJT&=}oS>tX|@Yd)} z_fTSYuA`$vku8J`62BJL;>0N%`Vl$HM}#o!+iW(A&Z@(oCnO|%CRWeN=|rYl>6Vt3 z4Mr0;iVL0V*RO9Cht3c%fzd~T6cG>N^fpYbj;>rNRp=DNf8^pmQ`(ME;_N8QynxD4 zKz1(^*SU|ZJXb=cD&ZO$8u}nADr%7rtNtk=gYXh|y1Tozh`SamNl8go(PgM1&iyhy zSf+@hn9w2aa0_S8UJ=CcdLJs{upMuSW8m6-`}Ubdt0tN4(@EZW^R;W&#_2wwuC7iN z64dPbCN~eEHPuC(3$p{Z~O#~yyh>wqt_LV#WWO;eH z;uXQEi(};%a{zI?Vx35;BqM8375Urh2qi+Jy5zI6vZ6)s zh;}LNr+DR&n|v>SLtw*1D6Y&m@|@GBPxp!4n~FSU@fY8)#|!eYVmFy1_9GJ@;9=hp zK1Qx3`#(nVD9G&4p+m~$_RE#OqYMiLfUaB1&COjc@{ZS$;7RR^NkHc#(n(ZQRIKqL z@k4<#9>CT5{eXWyVmU^Aef>GzyaT;>aj1-tN)w2}Bv30L?-0*)O!maW@C@W*gN95wMt z*Y-z{@CJ5vLkES9Tk-kjNfPhu?7ZSayo0+2BSt!A2pTDbcce~hYir-q?I`HxE(Z=A zXs3#)Q)b+R6VrW6cKZdKCji&>E+IrVjJ9mqlBw&7j5Mn(7RxD@j@b`@r?24Ce!k}q z3vh9{=$FXfFbR}pX0tiY7hRK0U8H-RcOebe+K57)xQ%HwTHQ=y5w`&gRZ~j(Yj}~A zl$52Zsi~&$@bD0K(gM-3v9WtduDZIqZb?t%GTOMoI>^B@z$fyekBJ`oi`-0F;^)Gm zjr0FLov(sg|MkI>h~U2Z^W2nO?ge4wAOHXW07*qoM6N<$f`WtTQ~&?~ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/default-7.png new file mode 100644 index 0000000000000000000000000000000000000000..b9079ad5d5f863ccbbbeac6ab2677ac0d2645698 GIT binary patch literal 1452 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 n?OHhGXep*ZTPPG6 z96*o~Ka@eKiOiH35FtuXA%TyY06%;%fA9$jL;O%6p$U)@HP(nCkWvBxO@t7JLo&jBQyWN|r{ch6hwv-du0@2lV4-PIkJVTV8TdcDlE zHUN9TS%53R8Q=i0C7*c=FajO`wgC6o^^r7LL4u9|Pr%CzJ_65g1Lgn=fF-~R;343N zmWTD|1{y4@XBK=F@FpXB5Cch%17-mmY$vRfwfX_Q9=G--K&;Y=US3}IadB~OF)=Y- zo}Ql09v&V}%I8;BR<@RxmkkRG3tNMOgX^80oogyzgKlyIa0~d{mm&*9&%otvKoq@S zP*C8Po}L~Q931Q?YD%NgxHCLFJlECLHCIzpGpG3TAYcG}dsl^+MY4?OWk4vumz|yM zU0q$B;Ns%qbPztUxw*O3*w{E&R#rAiZr=iQ0IzjTWR*m5yIWgZqaq_CgAOAlO;1lR z#>U3BFD)(Yuv@Z(Ep`kRmx*cW?MJs1zWwjHZ{u$5*{2tO& zM2Vh)OW1yhqE9w7Gz@rqdw&7z)rN(IT^$=6o7?L-bS-%9GJYq!^$y@giRG3}K^4QRZOk=;(+P-TJ(|ymkdq z^A=`iX7+1Y12YT;GMoAXUQlxbFF*+X5*ixnl$e+pEJAc(U|_Pjxp|RI(GvB5&j6-K z$Kb(0(y~@@ad9}A27w{%&17L>%T6cLsH&>EBpM(^MMZ;T`W@g;7R}TA=Ire3nwGT! z0s_3jSbSG;x6e__q6m3|TU9hgp;bplMz*-y0d51)$3zd9hQVOi(YhuaI=*D;LCIcV z0!5dSlHx6D)!N$HaWWkRqDC^8(x4Dzl*a{1)*MLj@$r75`H=+GG*eS_oRlGpu0WOSCPWCiWQbOqp6&0EOr8s14>8`oDx;l%j zO;Q-T!D&wRgzPcd*Vjk4-JoJse;&B|5J1T9q|E^z@&731&jX&54P3P{MeR%WRnrj{ z@Rbc*mGQ(zxqVts89rcpsgIu}zhlq_b3r5{)YsS7cgTsjZKDhfEJH*V3=9lBPo}Ri zjiF;DkjVvdb91GvN>bt~>I_cStja+W3RxCUI}9E+eI0NS2%lqxrD>$5rV5L8%->|X z0V(kjC2O*7eSQ5LN`XVQqqerTF;VMQ zR8&yw`CCv>nnzDh4;AdIX0l9l{LcSUQc^Om*oHkvX=!PKg0Qqz`3@69uId<39jg}X zS?yJW0Eu>OS7J;}O)WsF{sUxy&o?$U3^Hqsj*iZ99)M(QNtrluvWzeW#;5#)V5u9s zM=)SVz9D1-IX`6$hHWC;wkxd3X4=ZSy1HZs2M7EAqf-bmvj)XxE2+W00*()!scu;p zXlv>&W5XJV=xFw>(WLZkX&P@t!xQP~-xb+I6BY-;^L{YrMsnkt+wp>M_8{EN~O1jQh5XIM=AM?&4=F#5X-efHT2G4jbi!w4Y zhLp`4z_8B3J1$wvIC4EiEm{d$oQzNi2!TmYZrnEU5qs)^eyUS^oOd6!H1_ z`BByqz7>)(Fa2M9!(H+oGBg=jEL*B$aYLa_Z3P|`o z)32V)HPqho$eCgySO5S307*qoM6N<$ Ef@aS_IsgCw literal 0 HcmV?d00001 From ecfc6dfa3d41f27190381abe28f9dd28e86e46bd Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 21:41:29 +0800 Subject: [PATCH 113/189] CA1825: use Array.Empty. --- CodeAnalysis/osu.ruleset | 3 --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 3 ++- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 4 ++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 ++- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 3 ++- osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 3 ++- .../Visual/Multiplayer/TestSceneMatchParticipants.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs | 2 +- .../Visual/Online/TestSceneUserProfilePreviousUsernames.cs | 2 +- osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs | 2 +- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 2 +- osu.Game/Beatmaps/BeatmapInfo.cs | 4 ++-- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 2 +- osu.Game/Overlays/Mods/ModSection.cs | 2 +- osu.Game/Overlays/Music/PlaylistList.cs | 2 +- osu.Game/Overlays/UserProfileOverlay.cs | 6 ++---- osu.Game/Rulesets/Mods/Mod.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 4 ++-- osu.Game/Screens/Play/SquareGraph.cs | 2 +- osu.Game/Screens/Select/Details/FailRetryGraph.cs | 4 ++-- 20 files changed, 28 insertions(+), 29 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 9bcca40983..61e6520beb 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -57,7 +57,4 @@ - - - \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 506fa23fa9..bf5b00528b 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Difficulty; using osu.Game.Scoring; +using System; namespace osu.Game.Rulesets.Catch { @@ -112,7 +113,7 @@ namespace osu.Game.Rulesets.Catch }; default: - return new Mod[] { }; + return Array.Empty(); } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index a96c79b40b..3e3dc5cf66 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -158,7 +158,7 @@ namespace osu.Game.Rulesets.Mania }; default: - return new Mod[] { }; + return Array.Empty(); } } @@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Mania return stage1Bindings.Concat(stage2Bindings); } - return new KeyBinding[0]; + return Array.Empty(); } public override string GetVariantName(int variant) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 2f43909332..b182e5a658 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -26,6 +26,7 @@ using osu.Game.Rulesets.Osu.Difficulty; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Scoring; using osu.Game.Skinning; +using System; namespace osu.Game.Rulesets.Osu { @@ -149,7 +150,7 @@ namespace osu.Game.Rulesets.Osu }; default: - return new Mod[] { }; + return Array.Empty(); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ab9c95159c..0b4cb9801e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Scoring; +using System; namespace osu.Game.Rulesets.Taiko { @@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko }; default: - return new Mod[] { }; + return Array.Empty(); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 875e7b9758..4c5c18f38a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.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; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay requestCount = 0; increment = skip_time; - Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new Mod[] { }, 0) + Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), Array.Empty(), 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs index 50df4022dc..1ac914e27d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchParticipants.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep(@"set max", () => Room.MaxParticipants.Value = 10); - AddStep(@"clear users", () => Room.Participants.Value = new User[] { }); + AddStep(@"clear users", () => Room.Participants.Value = System.Array.Empty()); AddStep(@"set max to null", () => Room.MaxParticipants.Value = null); } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 98da63508b..15f9c9a013 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online }, Title = "osu!volunteer", Colour = "ff0000", - Achievements = new User.UserAchievement[0], + Achievements = Array.Empty(), }; public TestSceneUserProfileOverlay() diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs index d09a50b12c..048a1950fd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online new User { PreviousUsernames = new[] { "longusername", "longerusername" } }, new User { PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } }, new User { PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } }, - new User { PreviousUsernames = new string[0] }, + new User { PreviousUsernames = Array.Empty() }, null }; diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index ba63013886..f3eecf8afe 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tournament.Screens.Editors get { if (editorInfo == null) - return new MenuItem[0]; + return Array.Empty(); return new MenuItem[] { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index ded21730f3..031d6bf3d2 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -192,7 +192,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components get { if (editorInfo == null) - return new MenuItem[0]; + return Array.Empty(); return new MenuItem[] { diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 6e82c465dc..bcc9ab885e 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -98,7 +98,7 @@ namespace osu.Game.Beatmaps { if (string.IsNullOrEmpty(value)) { - Bookmarks = new int[0]; + Bookmarks = Array.Empty(); return; } @@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps } [NotMapped] - public int[] Bookmarks { get; set; } = new int[0]; + public int[] Bookmarks { get; set; } = Array.Empty(); public double DistanceSpacing { get; set; } public int BeatDivisor { get; set; } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 59a27e3fde..9ea254b23f 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps private class DummyRuleset : Ruleset { - public override IEnumerable GetModsFor(ModType type) => new Mod[] { }; + public override IEnumerable GetModsFor(ModType type) => Array.Empty(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) { diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index c55d1d8f70..7235a18a23 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Mods } } - private ModButton[] buttons = { }; + private ModButton[] buttons = Array.Empty(); protected override bool OnKeyDown(KeyDownEvent e) { diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 83528298b1..3cd04ac809 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -239,7 +239,7 @@ namespace osu.Game.Overlays.Music private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren { - public IEnumerable FilterTerms => new string[] { }; + public IEnumerable FilterTerms => Array.Empty(); public bool MatchingFilter { diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 468eb22b01..b5e7b8bedb 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.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; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -55,10 +56,7 @@ namespace osu.Game.Overlays new BeatmapsSection(), new KudosuSection() } - : new ProfileSection[] - { - //new AboutSection(), - }; + : Array.Empty(); tabs = new ProfileTabControl { diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 1c280c820d..b780ec9e76 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mods /// The mods this mod cannot be enabled with. /// [JsonIgnore] - public virtual Type[] IncompatibleMods => new Type[] { }; + public virtual Type[] IncompatibleMods => Array.Empty(); ///

/// Creates a copy of this initialised to a default state. diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 45aa904b98..2550f69286 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets /// /// The legacy enum which will be converted /// An enumerable of constructed s - public virtual IEnumerable ConvertLegacyMods(LegacyMods mods) => new Mod[] { }; + public virtual IEnumerable ConvertLegacyMods(LegacyMods mods) => Array.Empty(); public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First(); @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets /// /// A variant. /// A list of valid s. - public virtual IEnumerable GetDefaultKeyBindings(int variant = 0) => new KeyBinding[] { }; + public virtual IEnumerable GetDefaultKeyBindings(int variant = 0) => Array.Empty(); /// /// Gets the name for a key binding variant. This is used for display in the settings overlay. diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index 715ba3c065..a667466965 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play } } - private float[] calculatedValues = { }; // values but adjusted to fit the amount of columns + private float[] calculatedValues = Array.Empty(); // values but adjusted to fit the amount of columns private int[] values; diff --git a/osu.Game/Screens/Select/Details/FailRetryGraph.cs b/osu.Game/Screens/Select/Details/FailRetryGraph.cs index 121f8efe5a..134fd0598a 100644 --- a/osu.Game/Screens/Select/Details/FailRetryGraph.cs +++ b/osu.Game/Screens/Select/Details/FailRetryGraph.cs @@ -27,8 +27,8 @@ namespace osu.Game.Screens.Select.Details metrics = value; - var retries = Metrics?.Retries ?? new int[0]; - var fails = Metrics?.Fails ?? new int[0]; + var retries = Metrics?.Retries ?? Array.Empty(); + var fails = Metrics?.Fails ?? Array.Empty(); float maxValue = fails.Any() ? fails.Zip(retries, (fail, retry) => fail + retry).Max() : 0; failGraph.MaxValue = maxValue; From d7b3578cc61f6cd99d940443ef0d14e8faeccbb2 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 21:52:05 +0800 Subject: [PATCH 114/189] CA2201: throw correct exception type. --- CodeAnalysis/osu.ruleset | 3 +++ osu.Game.Tests/Visual/TestSceneOsuGame.cs | 4 ++-- osu.Game/Online/API/APIRequest.cs | 10 +++++++++- .../Rulesets/Difficulty/Utils/LimitedCapacityStack.cs | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 61e6520beb..b82799fea4 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -57,4 +57,7 @@ + + + \ No newline at end of file diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index e495b2a95a..492494ada3 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual foreach (var type in requiredGameDependencies) { if (game.Dependencies.Get(type) == null) - throw new Exception($"{type} has not been cached"); + throw new InvalidOperationException($"{type} has not been cached"); } return true; @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual foreach (var type in requiredGameBaseDependencies) { if (gameBase.Dependencies.Get(type) == null) - throw new Exception($"{type} has not been cached"); + throw new InvalidOperationException($"{type} has not been cached"); } return true; diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index b424e0f086..fcbd4d314a 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -122,7 +122,7 @@ namespace osu.Game.Online.API // attempt to decode a displayable error string. var error = JsonConvert.DeserializeObject(responseString); if (error != null) - e = new Exception(error.ErrorMessage, e); + e = new APIException(error.ErrorMessage, e); } catch { @@ -154,6 +154,14 @@ namespace osu.Game.Online.API } } + public class APIException : InvalidOperationException + { + public APIException(string messsage, Exception innerException) + : base(messsage, innerException) + { + } + } + public delegate void APIFailureHandler(Exception e); public delegate void APISuccessHandler(); diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs index d47caf409b..3cab04d904 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils get { if (i < 0 || i > Count - 1) - throw new IndexOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(i)); i += marker; if (i > capacity - 1) From 3c39fde7ff938f0397639cec8f7e871fdf8c4b9f Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 21:59:49 +0800 Subject: [PATCH 115/189] CA1065: throw NotSupportedException in properties. --- CodeAnalysis/osu.ruleset | 1 - osu.Game/Rulesets/UI/DrawableRuleset.cs | 28 ++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index b82799fea4..2f072ffa45 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -39,7 +39,6 @@ - diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5033fd0686..0bb99517ef 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -515,34 +515,34 @@ namespace osu.Game.Rulesets.UI public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name); - public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + public IEnumerable GetAvailableResources() => throw new NotSupportedException(); - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); - public BindableNumber Volume => throw new NotImplementedException(); + public BindableNumber Volume => throw new NotSupportedException(); - public BindableNumber Balance => throw new NotImplementedException(); + public BindableNumber Balance => throw new NotSupportedException(); - public BindableNumber Frequency => throw new NotImplementedException(); + public BindableNumber Frequency => throw new NotSupportedException(); - public BindableNumber Tempo => throw new NotImplementedException(); + public BindableNumber Tempo => throw new NotSupportedException(); - public IBindable GetAggregate(AdjustableProperty type) => throw new NotImplementedException(); + public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException(); - public IBindable AggregateVolume => throw new NotImplementedException(); + public IBindable AggregateVolume => throw new NotSupportedException(); - public IBindable AggregateBalance => throw new NotImplementedException(); + public IBindable AggregateBalance => throw new NotSupportedException(); - public IBindable AggregateFrequency => throw new NotImplementedException(); + public IBindable AggregateFrequency => throw new NotSupportedException(); - public IBindable AggregateTempo => throw new NotImplementedException(); + public IBindable AggregateTempo => throw new NotSupportedException(); public int PlaybackConcurrency { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } public void Dispose() From 09257b0c6dd17130b5d7c6e9f31726328e9bb368 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 22:07:46 +0800 Subject: [PATCH 116/189] CA1820: use IsNullOrEmpty. --- CodeAnalysis/osu.ruleset | 1 - osu.Game/Overlays/DirectOverlay.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 2f072ffa45..5329e84024 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -48,7 +48,6 @@ - diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index aedbd1b08b..9daf55c796 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays Filter.Search.Current.ValueChanged += text => { - if (text.NewValue != string.Empty) + if (!string.IsNullOrEmpty(text.NewValue)) { Header.Tabs.Current.Value = DirectTab.Search; From d5994ed4845b196b249f0c51db70d999ccbe359d Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 22:21:21 +0800 Subject: [PATCH 117/189] CA2208: create exceptions correctly. --- CodeAnalysis/osu.ruleset | 1 - osu.Game/Graphics/ScreenshotManager.cs | 2 +- osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs | 2 +- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- osu.Game/Screens/Play/ReplayPlayerLoader.cs | 2 +- osu.Game/Users/UserPanel.cs | 2 +- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 5329e84024..2a0c6fb928 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -51,7 +51,6 @@ - diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 02d928ec66..b9151b7393 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -119,7 +119,7 @@ namespace osu.Game.Graphics break; default: - throw new ArgumentOutOfRangeException(nameof(screenshotFormat)); + throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}."); } notificationOverlay.Post(new SimpleNotification diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs index 3cab04d904..1fc5abce90 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils public LimitedCapacityStack(int capacity) { if (capacity < 0) - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(capacity)); this.capacity = capacity; array = new T[capacity]; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ed48ddbc2f..386805d7e5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -150,8 +150,8 @@ namespace osu.Game.Rulesets.Objects.Drawables if (HitObject.SampleControlPoint == null) { - throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." - + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); + throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).ToArray(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 7fddb442d1..bdd019719b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Objects.Legacy int repeatCount = Parsing.ParseInt(split[6]); if (repeatCount > 9000) - throw new ArgumentOutOfRangeException(nameof(repeatCount), @"Repeat count is way too high"); + throw new FormatException(@"Repeat count is way too high"); // osu-stable treated the first span of the slider as a repeat, but no repeats are happening repeatCount = Math.Max(0, repeatCount - 1); diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 39d67f1071..018b50bd3d 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Scoring return miss; default: - throw new ArgumentException(nameof(result)); + throw new ArgumentException("Unknown enum member", nameof(result)); } } diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs index 86179ef067..c8ca604902 100644 --- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs +++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play : base(() => new ReplayPlayer(score)) { if (score.Replay == null) - throw new ArgumentNullException(nameof(score.Replay), $"{nameof(score)} must have a non-null {nameof(score.Replay)}."); + throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score)); scoreInfo = score.ScoreInfo; } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index c63c12773e..6ddbc13a06 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -63,7 +63,7 @@ namespace osu.Game.Users private void load(UserProfileOverlay profile) { if (colours == null) - throw new ArgumentNullException(nameof(colours)); + throw new InvalidOperationException($"{nameof(colours)} not initialized!"); FillFlowContainer infoContainer; From e46f6627e4f0a932a5d52c255810d6277a7263c7 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 22:26:10 +0800 Subject: [PATCH 118/189] CA1052: make type static. --- CodeAnalysis/osu.ruleset | 1 - osu.Game/IO/Legacy/SerializationReader.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 2a0c6fb928..0756adb05c 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -9,7 +9,6 @@ - diff --git a/osu.Game/IO/Legacy/SerializationReader.cs b/osu.Game/IO/Legacy/SerializationReader.cs index 82b2c4be32..aeb3ce754d 100644 --- a/osu.Game/IO/Legacy/SerializationReader.cs +++ b/osu.Game/IO/Legacy/SerializationReader.cs @@ -192,7 +192,7 @@ namespace osu.Game.IO.Legacy } } - public class DynamicDeserializer + public static class DynamicDeserializer { private static VersionConfigToNamespaceAssemblyObjectBinder versionBinder; private static BinaryFormatter formatter; From caf3f774baac67213e470d66f5e21e8e8f7616c5 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2019 22:39:09 +0800 Subject: [PATCH 119/189] CA1309: compare strings correctly. --- CodeAnalysis/osu.ruleset | 1 + osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 6 +++--- osu.Game/Screens/Select/FilterCriteria.cs | 2 +- osu.Game/Skinning/LegacySkinResourceStore.cs | 2 +- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 2 +- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 2 +- 9 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 0756adb05c..1aa8f66c93 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -54,6 +54,7 @@ + \ No newline at end of file diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 4924842e81..f9d71a2a6e 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps } } - private string getPathForFile(string filename) => BeatmapSetInfo.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + private string getPathForFile(string filename) => BeatmapSetInfo.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; private TextureStore textureStore; diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 42865c686c..393bcfdb3c 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -70,6 +70,6 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is SampleControlPoint otherTyped && - string.Equals(SampleBank, otherTyped.SampleBank) && SampleVolume == otherTyped.SampleVolume; + SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume; } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3279af05b6..332b3e3f05 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Scoring } protected override IEnumerable GetStableImportPaths(Storage stableStorage) - => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.InvariantCultureIgnoreCase) ?? false)); + => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 35816fe620..301d0d4dae 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -37,13 +37,13 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Artist: - return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); case SortMode.Title: - return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); case SortMode.Author: - return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.InvariantCultureIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); case SortMode.DateAdded: return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index e3ad76ac35..c4d9996377 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Select public string SearchTerm; - public bool Equals(OptionalTextFilter other) => SearchTerm?.Equals(other.SearchTerm) ?? true; + public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; } } } diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs index 79a4e2e932..249d48b34b 100644 --- a/osu.Game/Skinning/LegacySkinResourceStore.cs +++ b/osu.Game/Skinning/LegacySkinResourceStore.cs @@ -34,7 +34,7 @@ namespace osu.Game.Skinning } private string getPathForFile(string filename) => - source.Files.Find(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + source.Files.Find(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; public override IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index de3077c025..4f8e39fa1b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -71,7 +71,7 @@ namespace osu.Game.Storyboards.Drawables { var framePath = Animation.Path.Replace(".", frame + "."); - var path = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + var path = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; if (path == null) continue; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 3a117d1713..ff48dab7e5 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -66,7 +66,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(IBindable beatmap, TextureStore textureStore) { - var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; if (path == null) return; From 61a6106e5270f692b19fe7c265b990e64254ff73 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 3 Dec 2019 19:20:49 +0800 Subject: [PATCH 120/189] CA2200: don't explictly throw caught exception. --- CodeAnalysis/osu.ruleset | 1 - osu.Game/Online/API/APIAccess.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 1aa8f66c93..0ec2f24797 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -38,7 +38,6 @@ - diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 1c45d26afd..8bfc28e774 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; @@ -249,7 +250,7 @@ namespace osu.Game.Online.API catch { // if we couldn't deserialize the error message let's throw the original exception outwards. - throw e; + e.Rethrow(); } } From 40b43b85f17499d0e772b03ebcaf9b02434f343c Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 10 Dec 2019 21:04:26 +0800 Subject: [PATCH 121/189] CA1715: use prefix for generic parameters. --- CodeAnalysis/osu.ruleset | 1 - .../Objects/Drawables/DrawableTaikoHitObject.cs | 8 ++++---- ...MutableDatabaseBackedStoreWithFileIncludes.cs | 6 +++--- .../UserInterfaceV2/LabelledComponent.cs | 6 +++--- osu.Game/IO/Legacy/SerializationReader.cs | 6 +++--- osu.Game/IO/Legacy/SerializationWriter.cs | 4 ++-- osu.Game/Online/Leaderboards/Leaderboard.cs | 10 +++++----- osu.Game/Overlays/OverlayHeaderTabControl.cs | 2 +- osu.Game/Overlays/OverlayTabControl.cs | 8 ++++---- osu.Game/Overlays/Settings/SettingsSlider.cs | 16 ++++++++-------- osu.Game/Overlays/UserProfileOverlay.cs | 2 +- 11 files changed, 34 insertions(+), 35 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index 0ec2f24797..d497365f87 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -17,7 +17,6 @@ - diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 0db6498c12..2da5a9c403 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -105,19 +105,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject - where TaikoHitType : TaikoHitObject + public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject + where TTaikoHit : TaikoHitObject { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - public new TaikoHitType HitObject; + public new TTaikoHit HitObject; protected readonly Vector2 BaseSize; protected readonly TaikoPiece MainPiece; private readonly Container strongHitContainer; - protected DrawableTaikoHitObject(TaikoHitType hitObject) + protected DrawableTaikoHitObject(TTaikoHit hitObject) : base(hitObject) { HitObject = hitObject; diff --git a/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs b/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs index 5d6ff6b09b..102081cd65 100644 --- a/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs +++ b/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs @@ -7,9 +7,9 @@ using osu.Framework.Platform; namespace osu.Game.Database { - public abstract class MutableDatabaseBackedStoreWithFileIncludes : MutableDatabaseBackedStore - where T : class, IHasPrimaryKey, ISoftDelete, IHasFiles - where U : INamedFileInfo + public abstract class MutableDatabaseBackedStoreWithFileIncludes : MutableDatabaseBackedStore + where T : class, IHasPrimaryKey, ISoftDelete, IHasFiles + where TFileInfo : INamedFileInfo { protected MutableDatabaseBackedStoreWithFileIncludes(IDatabaseContextFactory contextFactory, Storage storage = null) : base(contextFactory, storage) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs index 1819b36667..dd6a902989 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs @@ -7,15 +7,15 @@ using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 { - public abstract class LabelledComponent : LabelledDrawable, IHasCurrentValue - where T : Drawable, IHasCurrentValue + public abstract class LabelledComponent : LabelledDrawable, IHasCurrentValue + where TDrawable : Drawable, IHasCurrentValue { protected LabelledComponent(bool padded) : base(padded) { } - public Bindable Current + public Bindable Current { get => Component.Current; set => Component.Current = value; diff --git a/osu.Game/IO/Legacy/SerializationReader.cs b/osu.Game/IO/Legacy/SerializationReader.cs index aeb3ce754d..17cbd19838 100644 --- a/osu.Game/IO/Legacy/SerializationReader.cs +++ b/osu.Game/IO/Legacy/SerializationReader.cs @@ -116,13 +116,13 @@ namespace osu.Game.IO.Legacy } /// Reads a generic Dictionary from the buffer. - public IDictionary ReadDictionary() + public IDictionary ReadDictionary() { int count = ReadInt32(); if (count < 0) return null; - IDictionary d = new Dictionary(); - for (int i = 0; i < count; i++) d[(T)ReadObject()] = (U)ReadObject(); + IDictionary d = new Dictionary(); + for (int i = 0; i < count; i++) d[(TKey)ReadObject()] = (TValue)ReadObject(); return d; } diff --git a/osu.Game/IO/Legacy/SerializationWriter.cs b/osu.Game/IO/Legacy/SerializationWriter.cs index f30e4492af..c75de93bc8 100644 --- a/osu.Game/IO/Legacy/SerializationWriter.cs +++ b/osu.Game/IO/Legacy/SerializationWriter.cs @@ -102,7 +102,7 @@ namespace osu.Game.IO.Legacy } /// Writes a generic IDictionary to the buffer. - public void Write(IDictionary d) + public void Write(IDictionary d) { if (d == null) { @@ -112,7 +112,7 @@ namespace osu.Game.IO.Legacy { Write(d.Count); - foreach (KeyValuePair kvp in d) + foreach (KeyValuePair kvp in d) { WriteObject(kvp.Key); WriteObject(kvp.Value); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 94c50185da..9c48ebd09b 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Online.Leaderboards { - public abstract class Leaderboard : Container, IOnlineComponent + public abstract class Leaderboard : Container, IOnlineComponent { private const double fade_duration = 300; @@ -39,9 +39,9 @@ namespace osu.Game.Online.Leaderboards protected override Container Content => content; - private IEnumerable scores; + private IEnumerable scores; - public IEnumerable Scores + public IEnumerable Scores { get => scores; set @@ -288,7 +288,7 @@ namespace osu.Game.Online.Leaderboards /// /// A callback which should be called when fetching is completed. Scheduling is not required. /// An responsible for the fetch operation. This will be queued and performed automatically. - protected abstract APIRequest FetchScores(Action> scoresCallback); + protected abstract APIRequest FetchScores(Action> scoresCallback); private Placeholder currentPlaceholder; @@ -359,6 +359,6 @@ namespace osu.Game.Online.Leaderboards } } - protected abstract LeaderboardScore CreateDrawableScore(ScoreInfo model, int index); + protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); } } diff --git a/osu.Game/Overlays/OverlayHeaderTabControl.cs b/osu.Game/Overlays/OverlayHeaderTabControl.cs index 5b56771dc1..7d0cdad6d8 100644 --- a/osu.Game/Overlays/OverlayHeaderTabControl.cs +++ b/osu.Game/Overlays/OverlayHeaderTabControl.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays AccentColour = AccentColour, }; - private class OverlayHeaderTabItem : OverlayTabItem + private class OverlayHeaderTabItem : OverlayTabItem { public OverlayHeaderTabItem(string value) : base(value) diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs index 20649c8a74..4c396eabc1 100644 --- a/osu.Game/Overlays/OverlayTabControl.cs +++ b/osu.Game/Overlays/OverlayTabControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays foreach (TabItem tabItem in TabContainer) { - ((OverlayTabItem)tabItem).AccentColour = value; + ((OverlayTabItem)tabItem).AccentColour = value; } } } @@ -59,9 +59,9 @@ namespace osu.Game.Overlays protected override Dropdown CreateDropdown() => null; - protected override TabItem CreateTabItem(T value) => new OverlayTabItem(value); + protected override TabItem CreateTabItem(T value) => new OverlayTabItem(value); - protected class OverlayTabItem : TabItem + protected class OverlayTabItem : TabItem { private readonly ExpandingBar bar; @@ -84,7 +84,7 @@ namespace osu.Game.Overlays } } - public OverlayTabItem(U value) + public OverlayTabItem(T value) : base(value) { AutoSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index 20e08c0cd8..96c0279a7b 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -12,11 +12,11 @@ namespace osu.Game.Overlays.Settings { } - public class SettingsSlider : SettingsItem - where T : struct, IEquatable, IComparable, IConvertible - where U : OsuSliderBar, new() + public class SettingsSlider : SettingsItem + where TValue : struct, IEquatable, IComparable, IConvertible + where TSlider : OsuSliderBar, new() { - protected override Drawable CreateControl() => new U + protected override Drawable CreateControl() => new TSlider { Margin = new MarginPadding { Top = 5, Bottom = 5 }, RelativeSizeAxes = Axes.X @@ -24,14 +24,14 @@ namespace osu.Game.Overlays.Settings public bool TransferValueOnCommit { - get => ((U)Control).TransferValueOnCommit; - set => ((U)Control).TransferValueOnCommit = value; + get => ((TSlider)Control).TransferValueOnCommit; + set => ((TSlider)Control).TransferValueOnCommit = value; } public float KeyboardStep { - get => ((U)Control).KeyboardStep; - set => ((U)Control).KeyboardStep = value; + get => ((TSlider)Control).KeyboardStep; + set => ((TSlider)Control).KeyboardStep = value; } } } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index b5e7b8bedb..a34fc619a8 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays AccentColour = colours.Seafoam; } - private class ProfileTabItem : OverlayTabItem + private class ProfileTabItem : OverlayTabItem { public ProfileTabItem(ProfileSection value) : base(value) From 5079feaad16ccd4df3b0e993d4bc7c6705deb686 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 14:04:03 +0900 Subject: [PATCH 122/189] Remove unnecessary string interpolation --- osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index d5d4c7e5ec..fede99f450 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("set dim level 0.6", () => userDimContainer.UserDimLevel.Value = test_user_dim); AddUntilStep("dim reached", () => userDimContainer.DimEqual(test_user_dim)); - AddStep($"ignore settings", () => userDimContainer.IgnoreUserSettings.Value = true); + AddStep("ignore settings", () => userDimContainer.IgnoreUserSettings.Value = true); AddUntilStep("no dim", () => userDimContainer.DimEqual(0)); AddStep("set break", () => isBreakTime.Value = true); AddAssert("no dim", () => userDimContainer.DimEqual(0)); From 946a202ee555cc26f12aabca4d9964dea2d99cdf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 15:34:16 +0900 Subject: [PATCH 123/189] Fix online replays not being available locally --- osu.Game/Beatmaps/BeatmapManager.cs | 4 +++- osu.Game/Database/DownloadableArchiveModelManager.cs | 6 ++++-- osu.Game/Scoring/ScoreManager.cs | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a2e750cac5..a10ad73817 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -158,7 +158,9 @@ namespace osu.Game.Beatmaps void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); } - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) => items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); /// /// Delete a beatmap difficulty. diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index 243060388f..5f688c149d 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -33,7 +33,8 @@ namespace osu.Game.Database private readonly MutableDatabaseBackedStoreWithFileIncludes modelStore; - protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) + protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, + IIpcHost importHost = null) : base(storage, contextFactory, modelStore, importHost) { this.api = api; @@ -124,7 +125,8 @@ namespace osu.Game.Database /// The whose existence needs to be checked. /// The usable items present in the store. /// Whether the exists. - protected abstract bool CheckLocalAvailability(TModel model, IQueryable items); + protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) + => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); public ArchiveDownloadRequest GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 3279af05b6..5c846e8062 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -69,6 +69,8 @@ namespace osu.Game.Scoring protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); - protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) => items.Any(s => s.Equals(model) && s.Files.Any()); + protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); } } From 59c3b39ed8a7ab63fe653ec60a0d9cdf3e4551af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 15:46:50 +0900 Subject: [PATCH 124/189] Add test --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 47 ++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 89b5db9e1b..a95e699470 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.IO.Archives; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -154,7 +156,30 @@ namespace osu.Game.Tests.Scores.IO } } - private async Task loadIntoOsu(OsuGameBase osu, ScoreInfo score) + [Test] + public async Task TestOnlineScoreIsAvailableLocally() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet")) + { + try + { + var osu = await loadOsu(host); + + await loadIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); + + var scoreManager = osu.Dependencies.Get(); + + // Note: A new score reference is used here since the import process mutates the original object to set an ID + Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineScoreID = 2 })); + } + finally + { + host.Exit(); + } + } + } + + private async Task loadIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { var beatmapManager = osu.Dependencies.Get(); @@ -165,7 +190,7 @@ namespace osu.Game.Tests.Scores.IO score.Ruleset = new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); - await scoreManager.Import(score); + await scoreManager.Import(score, archive); return scoreManager.GetAllUsableScores().FirstOrDefault(); } @@ -196,5 +221,23 @@ namespace osu.Game.Tests.Scores.IO Assert.IsTrue(task.Wait(timeout), failureMessage); } + + private class TestArchiveReader : ArchiveReader + { + public TestArchiveReader() + : base("test_archive") + { + } + + public override Stream GetStream(string name) => new MemoryStream(); + + public override void Dispose() + { + } + + public override IEnumerable Filenames => new[] { "test_file.osr" }; + + public override Stream GetUnderlyingStream() => new MemoryStream(); + } } } From 927ba4c1133088a67233a32fd56ccbe670c97500 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 17 Dec 2019 15:20:05 +0800 Subject: [PATCH 125/189] Update expected exception in test. --- osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs index 1c78b63499..d5ac38008e 100644 --- a/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs +++ b/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.NonVisual { Assert.AreEqual(0, stack.Count); - Assert.Throws(() => + Assert.Throws(() => { int unused = stack[0]; }); @@ -55,7 +55,7 @@ namespace osu.Game.Tests.NonVisual // e.g. indices 3, 4, 5, 6 (out of range) for (int i = stack.Count; i < stack.Count + capacity; i++) { - Assert.Throws(() => + Assert.Throws(() => { int unused = stack[i]; }); @@ -80,7 +80,7 @@ namespace osu.Game.Tests.NonVisual // e.g. indices 3, 4, 5, 6 (out of range) for (int i = stack.Count; i < stack.Count + capacity; i++) { - Assert.Throws(() => + Assert.Throws(() => { int unused = stack[i]; }); From 6da581f3fe86047762679905ca3ce14bbccb836b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 16:35:40 +0900 Subject: [PATCH 126/189] Snap based on end position/time of the previous object --- .../Edit/OsuDistanceSnapGrid.cs | 3 ++- .../Edit/OsuHitObjectComposer.cs | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index bde86a2890..ff3be97427 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using JetBrains.Annotations; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit public class OsuDistanceSnapGrid : CircularDistanceSnapGrid { public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject.StackedPosition, hitObject.StartTime, nextHitObject?.StartTime) + : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 675b09fc6d..a2c1a5f5f4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -92,7 +92,24 @@ namespace osu.Game.Rulesets.Osu.Edit return null; OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; - OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null; + + int targetIndex = sourceIndex + targetOffset; + OsuHitObject targetObject = null; + + // Keep advancing the target object while its start time falls before the end time of the source object + while (true) + { + if (targetIndex >= EditorBeatmap.HitObjects.Count) + break; + + if (EditorBeatmap.HitObjects[targetIndex].StartTime >= sourceObject.GetEndTime()) + { + targetObject = EditorBeatmap.HitObjects[targetIndex]; + break; + } + + targetIndex++; + } return new OsuDistanceSnapGrid(sourceObject, targetObject); } From 1e798a8dbe1c31244e52e7ab7d01ec96a5ce76ff Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 17:53:48 +0900 Subject: [PATCH 127/189] Add abstract implementation of slider path --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Drawables/Pieces/DrawableSliderPath.cs | 70 ++++++++++++++ .../Objects/Drawables/Pieces/SliderBody.cs | 92 ++++--------------- 3 files changed, 88 insertions(+), 76 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DrawableSliderPath.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 1e0402d492..e6ab2e580c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ApplySkin(skin, allowFallback); - Body.BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? SliderBody.DEFAULT_BORDER_SIZE; + Body.BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; sliderPathRadius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; updatePathRadius(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DrawableSliderPath.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DrawableSliderPath.cs new file mode 100644 index 0000000000..c31d6beb01 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DrawableSliderPath.cs @@ -0,0 +1,70 @@ +// 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.Lines; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public abstract class DrawableSliderPath : SmoothPath + { + protected const float BORDER_PORTION = 0.128f; + protected const float GRADIENT_PORTION = 1 - BORDER_PORTION; + + private const float border_max_size = 8f; + private const float border_min_size = 0f; + + private Color4 borderColour = Color4.White; + + public Color4 BorderColour + { + get => borderColour; + set + { + if (borderColour == value) + return; + + borderColour = value; + + InvalidateTexture(); + } + } + + private Color4 accentColour = Color4.White; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + + accentColour = value; + + InvalidateTexture(); + } + } + + private float borderSize = 1; + + public float BorderSize + { + get => borderSize; + set + { + if (borderSize == value) + return; + + if (value < border_min_size || value > border_max_size) + return; + + borderSize = value; + + InvalidateTexture(); + } + } + + protected float CalculatedBorderPortion => BorderSize * BORDER_PORTION; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 24a437c20e..dcaa9748e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; using osuTK; @@ -12,9 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public abstract class SliderBody : CompositeDrawable { - public const float DEFAULT_BORDER_SIZE = 1; - - private SliderPath path; + private DrawableSliderPath path; protected Path Path => path; @@ -80,19 +79,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } /// - /// Initialises a new , releasing all resources retained by the old one. + /// Initialises a new , releasing all resources retained by the old one. /// public virtual void RecyclePath() { - InternalChild = path = new SliderPath + InternalChild = path = CreateSliderPath().With(p => { - Position = path?.Position ?? Vector2.Zero, - PathRadius = path?.PathRadius ?? 10, - AccentColour = path?.AccentColour ?? Color4.White, - BorderColour = path?.BorderColour ?? Color4.White, - BorderSize = path?.BorderSize ?? DEFAULT_BORDER_SIZE, - Vertices = path?.Vertices ?? Array.Empty() - }; + p.Position = path?.Position ?? Vector2.Zero; + p.PathRadius = path?.PathRadius ?? 10; + p.AccentColour = path?.AccentColour ?? Color4.White; + p.BorderColour = path?.BorderColour ?? Color4.White; + p.BorderSize = path?.BorderSize ?? 1; + p.Vertices = path?.Vertices ?? Array.Empty(); + }); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => path.ReceivePositionalInputAt(screenSpacePos); @@ -103,77 +102,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// The vertices protected void SetVertices(IReadOnlyList vertices) => path.Vertices = vertices; - private class SliderPath : SmoothPath + protected virtual DrawableSliderPath CreateSliderPath() => new DefaultDrawableSliderPath(); + + protected class DefaultDrawableSliderPath : DrawableSliderPath { - private const float border_max_size = 8f; - private const float border_min_size = 0f; - - private const float border_portion = 0.128f; - private const float gradient_portion = 1 - border_portion; - private const float opacity_at_centre = 0.3f; private const float opacity_at_edge = 0.8f; - private Color4 borderColour = Color4.White; - - public Color4 BorderColour - { - get => borderColour; - set - { - if (borderColour == value) - return; - - borderColour = value; - - InvalidateTexture(); - } - } - - private Color4 accentColour = Color4.White; - - public Color4 AccentColour - { - get => accentColour; - set - { - if (accentColour == value) - return; - - accentColour = value; - - InvalidateTexture(); - } - } - - private float borderSize = DEFAULT_BORDER_SIZE; - - public float BorderSize - { - get => borderSize; - set - { - if (borderSize == value) - return; - - if (value < border_min_size || value > border_max_size) - return; - - borderSize = value; - - InvalidateTexture(); - } - } - - private float calculatedBorderPortion => BorderSize * border_portion; - protected override Color4 ColourAt(float position) { - if (calculatedBorderPortion != 0f && position <= calculatedBorderPortion) + if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion) return BorderColour; - position -= calculatedBorderPortion; - return new Color4(AccentColour.R, AccentColour.G, AccentColour.B, (opacity_at_edge - (opacity_at_edge - opacity_at_centre) * position / gradient_portion) * AccentColour.A); + position -= CalculatedBorderPortion; + return new Color4(AccentColour.R, AccentColour.G, AccentColour.B, (opacity_at_edge - (opacity_at_edge - opacity_at_centre) * position / GRADIENT_PORTION) * AccentColour.A); } } } From 8cd96acffc2e3f3fe61062983804da1a4db33ff9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Dec 2019 12:05:35 +0300 Subject: [PATCH 128/189] CounterPill implementation --- .../Online/TestSceneProfileCounterPill.cs | 42 +++++++++++++ .../Overlays/Profile/Sections/CounterPill.cs | 61 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs create mode 100644 osu.Game/Overlays/Profile/Sections/CounterPill.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs new file mode 100644 index 0000000000..468239cf08 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs @@ -0,0 +1,42 @@ +// 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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Overlays.Profile.Sections; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneProfileCounterPill : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CounterPill) + }; + + private readonly CounterPill pill; + private readonly BindableInt value = new BindableInt(); + + public TestSceneProfileCounterPill() + { + Child = pill = new CounterPill + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { BindTarget = value } + }; + } + + [Test] + public void TestVisibility() + { + AddStep("Set value to 0", () => value.Value = 0); + AddAssert("Check hidden", () => !pill.IsPresent); + AddStep("Set value to 10", () => value.Value = 10); + AddAssert("Check visible", () => pill.IsPresent); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/CounterPill.cs b/osu.Game/Overlays/Profile/Sections/CounterPill.cs new file mode 100644 index 0000000000..bd760c4139 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/CounterPill.cs @@ -0,0 +1,61 @@ +// 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.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Framework.Bindables; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Overlays.Profile.Sections +{ + public class CounterPill : CircularContainer + { + private const int duration = 200; + + public readonly BindableInt Current = new BindableInt(); + + private readonly OsuSpriteText counter; + + public CounterPill() + { + AutoSizeAxes = Axes.Both; + Alpha = 0; + Masking = true; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.05f) + }, + counter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(onCurrentChanged, true); + } + + private void onCurrentChanged(ValueChangedEvent value) + { + if (value.NewValue == 0) + { + this.FadeOut(duration, Easing.OutQuint); + return; + } + + counter.Text = value.NewValue.ToString(); + this.FadeIn(duration, Easing.OutQuint); + } + } +} From 9caed9e98a25993d6984eb74d541c9e3ad1c6012 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 18:16:25 +0900 Subject: [PATCH 129/189] Add legacy slider body support --- .../Objects/Drawables/DrawableSlider.cs | 6 ++- .../Drawables/Pieces/SnakingSliderBody.cs | 12 ++--- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 +- .../Skinning/LegacySliderBody.cs | 47 +++++++++++++++++++ .../Skinning/OsuLegacySkinTransformer.cs | 6 +++ 5 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index e6ab2e580c..46a219a69c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -24,9 +24,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSliderHead HeadCircle => headContainer.Child; public DrawableSliderTail TailCircle => tailContainer.Child; - public readonly SnakingSliderBody Body; public readonly SliderBall Ball; + public SnakingSliderBody Body => (SnakingSliderBody)skinnedBody.Drawable; + + private readonly SkinnableDrawable skinnedBody; private readonly Container headContainer; private readonly Container tailContainer; private readonly Container tickContainer; @@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { - Body = new SnakingSliderBody(s), + skinnedBody = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new SnakingSliderBody(), confineMode: ConfineMode.NoScaling), tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index f2150280b3..8a8668d6af 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -50,16 +51,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// private Vector2 snakedPathOffset; - private readonly Slider slider; - - public SnakingSliderBody(Slider slider) - { - this.slider = slider; - } + private Slider slider; [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject drawableObject) { + slider = (Slider)drawableObject.HitObject; + Refresh(); } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 8dd48eace0..4ea4220faf 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu ReverseArrow, HitCircleText, SliderFollowCircle, - SliderBall + SliderBall, + SliderBody, } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs new file mode 100644 index 0000000000..6a26529f4c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -0,0 +1,47 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.MathUtils; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public class LegacySliderBody : SnakingSliderBody + { + protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath(); + + private class LegacyDrawableSliderPath : DrawableSliderPath + { + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.70f); + + protected override Color4 ColourAt(float position) + { + if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion) + return BorderColour; + + position -= BORDER_PORTION; + + Color4 outerColour = AccentColour.Darken(0.1f); + Color4 innerColour = lighten(AccentColour, 0.5f); + + return Interpolation.ValueAt(position / GRADIENT_PORTION, outerColour, innerColour, 0, 1); + } + + /// + /// Lightens a colour in a way more friendly to dark or strong colours. + /// + private static Color4 lighten(Color4 color, float amount) + { + amount *= 0.5f; + return new Color4( + Math.Min(1, color.R * (1 + 0.5f * amount) + 1 * amount), + Math.Min(1, color.G * (1 + 0.5f * amount) + 1 * amount), + Math.Min(1, color.B * (1 + 0.5f * amount) + 1 * amount), + color.A); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index f5b7d9166f..71770dedce 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.SliderBody: + if (hasHitCircle.Value) + return new LegacySliderBody(); + + return null; + case OsuSkinComponents.HitCircle: if (hasHitCircle.Value) return new LegacyMainCirclePiece(); From 023892738af69c35902c39c43945906fb0b284ea Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Dec 2019 12:36:44 +0300 Subject: [PATCH 130/189] Integration into overlay --- .../Beatmaps/PaginatedBeatmapContainer.cs | 22 +++++++++++++++ .../Profile/Sections/PaginatedContainer.cs | 28 +++++++++++++++++-- .../Sections/Ranks/PaginatedScoreContainer.cs | 12 ++++++++ osu.Game/Users/User.cs | 18 ++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 919f8a2fa0..6684420cf4 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -38,5 +38,27 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }; + + protected override int GetCount(User user) + { + switch (type) + { + default: + case BeatmapSetType.Favourite: + return user.FavouriteBeatmapsetCount; + + case BeatmapSetType.Graveyard: + return user.GraveyardBeatmapsetCount; + + case BeatmapSetType.Loved: + return user.LovedBeatmapsetCount; + + case BeatmapSetType.RankedAndApproved: + return user.RankedAndApprovedBeatmapsetCount; + + case BeatmapSetType.Unranked: + return user.UnrankedBeatmapsetCount; + } + } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index dc1a847b14..94d5f99f86 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Profile.Sections private readonly OsuSpriteText missingText; private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; + private readonly BindableInt count = new BindableInt(); [Resolved] private IAPIProvider api { get; set; } @@ -44,11 +45,28 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Text = header, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), Margin = new MarginPadding { Top = 10, Bottom = 10 }, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = header, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), + }, + new CounterPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = { BindTarget = count } + } + } }, ItemsContainer = new FillFlowContainer { @@ -90,6 +108,8 @@ namespace osu.Game.Overlays.Profile.Sections VisiblePages = 0; ItemsContainer.Clear(); + count.Value = GetCount(e.NewValue); + if (e.NewValue != null) showMore(); } @@ -124,6 +144,8 @@ namespace osu.Game.Overlays.Profile.Sections }, loadCancellation.Token); }); + protected virtual int GetCount(User user) => 0; + protected abstract APIRequest> CreateRequest(); protected abstract Drawable CreateDrawableItem(TModel model); diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 5b58fc0930..fa74d76dfc 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -32,6 +32,18 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); + protected override int GetCount(User user) + { + switch (type) + { + default: + return 0; + + case ScoreType.Firsts: + return user.ScoresFirstCount; + } + } + protected override Drawable CreateDrawableItem(APILegacyScoreInfo model) { switch (type) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index b15789f324..ebd9dbecd1 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -126,6 +126,24 @@ namespace osu.Game.Users [JsonProperty(@"follower_count")] public int FollowerCount; + [JsonProperty(@"favourite_beatmapset_count")] + public int FavouriteBeatmapsetCount; + + [JsonProperty(@"graveyard_beatmapset_count")] + public int GraveyardBeatmapsetCount; + + [JsonProperty(@"loved_beatmapset_count")] + public int LovedBeatmapsetCount; + + [JsonProperty(@"ranked_and_approved_beatmapset_count")] + public int RankedAndApprovedBeatmapsetCount; + + [JsonProperty(@"unranked_beatmapset_count")] + public int UnrankedBeatmapsetCount; + + [JsonProperty(@"scores_first_count")] + public int ScoresFirstCount; + [JsonProperty] private string[] playstyle { From bc9177983adc209fa185cbf549de01bcbab77222 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Dec 2019 12:50:50 +0300 Subject: [PATCH 131/189] Fix possible null --- osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 94d5f99f86..a30ff786fb 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -108,10 +108,11 @@ namespace osu.Game.Overlays.Profile.Sections VisiblePages = 0; ItemsContainer.Clear(); - count.Value = GetCount(e.NewValue); - if (e.NewValue != null) + { showMore(); + count.Value = GetCount(e.NewValue); + } } private void showMore() From 7c2884700eaffe391e37fa79a80deb3a30a431aa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 19:29:27 +0900 Subject: [PATCH 132/189] Fix various display issues by abstracting further --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 5 +- .../Objects/Drawables/DrawableRepeatPoint.cs | 3 +- .../Objects/Drawables/DrawableSlider.cs | 52 +++++------------ .../Drawables/Pieces/PlaySliderBody.cs | 57 +++++++++++++++++++ .../Objects/Drawables/Pieces/SliderBody.cs | 2 +- .../Skinning/LegacySliderBody.cs | 2 +- 6 files changed, 78 insertions(+), 43 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 7e20feba02..0cca3ae40c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,6 +11,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Mods { @@ -57,8 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods slider.AccentColour.BindValueChanged(_ => { //will trigger on skin change. - slider.Body.AccentColour = slider.AccentColour.Value.Opacity(0); - slider.Body.BorderColour = slider.AccentColour.Value; + ((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0); + ((PlaySliderBody)slider.Body.Drawable).BorderColour = slider.AccentColour.Value; }, true); break; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 71cb9a9691..b81d94a673 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; using osuTK; using osu.Game.Skinning; @@ -98,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; - List curve = drawableSlider.Body.CurrentCurve; + List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 46a219a69c..03183beff1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -11,7 +11,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; using osuTK.Graphics; @@ -25,10 +24,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSliderTail TailCircle => tailContainer.Child; public readonly SliderBall Ball; + public readonly SkinnableDrawable Body; - public SnakingSliderBody Body => (SnakingSliderBody)skinnedBody.Drawable; + private PlaySliderBody sliderBody => (PlaySliderBody)Body.Drawable; - private readonly SkinnableDrawable skinnedBody; private readonly Container headContainer; private readonly Container tailContainer; private readonly Container tickContainer; @@ -39,10 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly IBindable positionBindable = new Bindable(); private readonly IBindable stackHeightBindable = new Bindable(); private readonly IBindable scaleBindable = new Bindable(); - private readonly IBindable pathVersion = new Bindable(); - - [Resolved(CanBeNull = true)] - private OsuRulesetConfigManager config { get; set; } public DrawableSlider(Slider s) : base(s) @@ -53,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { - skinnedBody = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new SnakingSliderBody(), confineMode: ConfineMode.NoScaling), + Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) @@ -72,28 +67,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - config?.BindWith(OsuRulesetSetting.SnakingInSliders, Body.SnakingIn); - config?.BindWith(OsuRulesetSetting.SnakingOutSliders, Body.SnakingOut); - positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); - scaleBindable.BindValueChanged(scale => - { - updatePathRadius(); - Ball.Scale = new Vector2(scale.NewValue); - }); + scaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue)); positionBindable.BindTo(HitObject.PositionBindable); stackHeightBindable.BindTo(HitObject.StackHeightBindable); scaleBindable.BindTo(HitObject.ScaleBindable); - pathVersion.BindTo(slider.Path.Version); - - pathVersion.BindValueChanged(_ => Body.Refresh()); AccentColour.BindValueChanged(colour => { - Body.AccentColour = colour.NewValue; - foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; }, true); @@ -171,16 +154,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); Ball.UpdateProgress(completionProgress); - Body.UpdateProgress(completionProgress); + sliderBody.UpdateProgress(completionProgress); foreach (DrawableHitObject hitObject in NestedHitObjects) { - if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(Body.SnakedStart ?? 0), slider.Path.PositionAt(Body.SnakedEnd ?? 0)); + if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(sliderBody.SnakedStart ?? 0), slider.Path.PositionAt(sliderBody.SnakedEnd ?? 0)); if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking; } - Size = Body.Size; - OriginPosition = Body.PathOffset; + Size = sliderBody.Size; + OriginPosition = sliderBody.PathOffset; if (DrawSize != Vector2.Zero) { @@ -194,28 +177,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override void OnKilled() { base.OnKilled(); - Body.RecyclePath(); + sliderBody.RecyclePath(); } - private float sliderPathRadius; - protected override void ApplySkin(ISkinSource skin, bool allowFallback) { base.ApplySkin(skin, allowFallback); - Body.BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; - sliderPathRadius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; - updatePathRadius(); - - Body.AccentColour = skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? AccentColour.Value; - Body.BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; - bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White; } - private void updatePathRadius() => Body.PathRadius = slider.Scale * sliderPathRadius; - protected override void CheckForResult(bool userTriggered, double timeOffset) { if (userTriggered || Time.Current < slider.EndTime) @@ -266,6 +238,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Body.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody.ReceivePositionalInputAt(screenSpacePos); + + private class DefaultSliderBody : PlaySliderBody + { + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs new file mode 100644 index 0000000000..aa9caf193e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs @@ -0,0 +1,57 @@ +// 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.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public abstract class PlaySliderBody : SnakingSliderBody + { + private IBindable scaleBindable; + private IBindable pathVersion; + private IBindable accentColour; + + [Resolved] + private DrawableHitObject drawableObject { get; set; } + + [Resolved(CanBeNull = true)] + private OsuRulesetConfigManager config { get; set; } + + private Slider slider; + private float defaultPathRadius; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + slider = (Slider)drawableObject.HitObject; + defaultPathRadius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; + + scaleBindable = slider.ScaleBindable.GetBoundCopy(); + scaleBindable.BindValueChanged(_ => updatePathRadius(), true); + + pathVersion = slider.Path.Version.GetBoundCopy(); + pathVersion.BindValueChanged(_ => Refresh()); + + accentColour = drawableObject.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true); + + config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn); + config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut); + + BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; + BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; + } + + private void updatePathRadius() + => PathRadius = defaultPathRadius * scaleBindable.Value; + + private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour) + => AccentColour = skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? defaultAccentColour; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index dcaa9748e9..8758a4a066 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected virtual DrawableSliderPath CreateSliderPath() => new DefaultDrawableSliderPath(); - protected class DefaultDrawableSliderPath : DrawableSliderPath + private class DefaultDrawableSliderPath : DrawableSliderPath { private const float opacity_at_centre = 0.3f; private const float opacity_at_edge = 0.8f; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 6a26529f4c..18a5d7a320 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -9,7 +9,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { - public class LegacySliderBody : SnakingSliderBody + public class LegacySliderBody : PlaySliderBody { protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath(); From f6cde911e2b4a13e57acb84863d9b524668c2985 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Dec 2019 13:41:28 +0300 Subject: [PATCH 133/189] Use switch expressions --- .../Beatmaps/PaginatedBeatmapContainer.cs | 28 ++++++------------- .../Sections/Ranks/PaginatedScoreContainer.cs | 14 +++------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 6684420cf4..fcd12e2b54 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -39,26 +39,14 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps Origin = Anchor.TopCentre, }; - protected override int GetCount(User user) + protected override int GetCount(User user) => type switch { - switch (type) - { - default: - case BeatmapSetType.Favourite: - return user.FavouriteBeatmapsetCount; - - case BeatmapSetType.Graveyard: - return user.GraveyardBeatmapsetCount; - - case BeatmapSetType.Loved: - return user.LovedBeatmapsetCount; - - case BeatmapSetType.RankedAndApproved: - return user.RankedAndApprovedBeatmapsetCount; - - case BeatmapSetType.Unranked: - return user.UnrankedBeatmapsetCount; - } - } + BeatmapSetType.Favourite => user.FavouriteBeatmapsetCount, + BeatmapSetType.Graveyard => user.GraveyardBeatmapsetCount, + BeatmapSetType.Loved => user.LovedBeatmapsetCount, + BeatmapSetType.RankedAndApproved => user.RankedAndApprovedBeatmapsetCount, + BeatmapSetType.Unranked => user.UnrankedBeatmapsetCount, + _ => 0 + }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index fa74d76dfc..e0f1c935da 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -32,17 +32,11 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); - protected override int GetCount(User user) + protected override int GetCount(User user) => type switch { - switch (type) - { - default: - return 0; - - case ScoreType.Firsts: - return user.ScoresFirstCount; - } - } + ScoreType.Firsts => user.ScoresFirstCount, + _ => 0 + }; protected override Drawable CreateDrawableItem(APILegacyScoreInfo model) { From 527ab1a72f08ce6c3c5eef7226ec166c9fb726b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 19:49:13 +0900 Subject: [PATCH 134/189] Fix traceable mod not working on skin change --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 15 ++++++++------- osu.Game/Skinning/SkinReloadableDrawable.cs | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 0cca3ae40c..cf1ce517e8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -55,13 +55,8 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSlider slider: - slider.AccentColour.BindValueChanged(_ => - { - //will trigger on skin change. - ((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0); - ((PlaySliderBody)slider.Body.Drawable).BorderColour = slider.AccentColour.Value; - }, true); - + slider.Body.OnSkinChanged += () => applySliderState(slider); + applySliderState(slider); break; case DrawableSpinner spinner: @@ -70,5 +65,11 @@ namespace osu.Game.Rulesets.Osu.Mods break; } } + + private void applySliderState(DrawableSlider slider) + { + ((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0); + ((PlaySliderBody)slider.Body.Drawable).BorderColour = slider.AccentColour.Value; + } } } diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index 6d0b22dd51..4a1aaa62bf 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -12,6 +12,11 @@ namespace osu.Game.Skinning /// public abstract class SkinReloadableDrawable : CompositeDrawable { + /// + /// Invoked when has changed. + /// + public event Action OnSkinChanged; + /// /// The current skin source. /// @@ -43,12 +48,18 @@ namespace osu.Game.Skinning private void onChange() => // schedule required to avoid calls after disposed. // note that this has the side-effect of components only performing a skin change when they are alive. - Scheduler.AddOnce(() => SkinChanged(CurrentSkin, allowDefaultFallback)); + Scheduler.AddOnce(skinChanged); protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); + skinChanged(); + } + + private void skinChanged() + { SkinChanged(CurrentSkin, allowDefaultFallback); + OnSkinChanged?.Invoke(); } /// @@ -66,6 +77,8 @@ namespace osu.Game.Skinning if (CurrentSkin != null) CurrentSkin.SourceChanged -= onChange; + + OnSkinChanged = null; } } } From 2d85145eeccde1868c1fb843c852450e37c2abf7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 19:52:33 +0900 Subject: [PATCH 135/189] Make legacy accent colour multiplicative --- osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 18a5d7a320..dea08f843e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private class LegacyDrawableSliderPath : DrawableSliderPath { - public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.70f); + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) { From 49bf8d27d10ad2ceaf9feefe737b2f60ce2dffdd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 20:08:13 +0900 Subject: [PATCH 136/189] Move CreateScoreProcessor() to Ruleset --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 5 +++++ osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 4 ---- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 6 ++++++ osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 4 ---- osu.Game.Rulesets.Osu/OsuRuleset.cs | 6 ++++++ osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 4 ---- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 5 +++++ osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs | 4 ---- osu.Game/Rulesets/Ruleset.cs | 7 +++++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 9 --------- osu.Game/Screens/Play/Player.cs | 2 +- 11 files changed, 30 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9fe287d1cc..ba5c1dee92 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -16,7 +16,9 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; +using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Catch @@ -24,6 +26,9 @@ namespace osu.Game.Rulesets.Catch public class CatchRuleset : Ruleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods); + + public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new CatchScoreProcessor(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 278ff97195..fdd820b891 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -10,10 +10,8 @@ using osu.Game.Replays; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Catch.Replays; -using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -32,8 +30,6 @@ namespace osu.Game.Rulesets.Catch.UI TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } - public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(Beatmap); - protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bcafadb4c5..dc98a063b4 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -25,6 +25,8 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Edit; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Mania @@ -32,7 +34,11 @@ namespace osu.Game.Rulesets.Mania public class ManiaRuleset : Ruleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods); + + public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ManiaScoreProcessor(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score); public const string SHORT_NAME = "mania"; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index cf1970c28b..2c497541a8 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -14,11 +14,9 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Replays; -using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -67,8 +65,6 @@ namespace osu.Game.Rulesets.Mania.UI protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); - public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(Beatmap); - public override int Variant => (int)(Beatmap.Stages.Count == 1 ? PlayfieldType.Single : PlayfieldType.Dual) + Beatmap.TotalColumns; protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index beaa788229..ff08d97385 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -23,7 +23,9 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Difficulty; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; @@ -32,7 +34,11 @@ namespace osu.Game.Rulesets.Osu public class OsuRuleset : Ruleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods); + + public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new OsuScoreProcessor(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap); + public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); public const string SHORT_NAME = "osu"; diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 039d38e4fd..a37ef8d9a0 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -14,8 +14,6 @@ using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osuTK; @@ -33,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor - public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(Beatmap); - protected override Playfield CreatePlayfield() => new OsuPlayfield(); protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 68e6a3b07e..59ce715a1f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -15,8 +15,10 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Difficulty; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko @@ -24,6 +26,9 @@ namespace osu.Game.Rulesets.Taiko public class TaikoRuleset : Ruleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); + + public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new TaikoScoreProcessor(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap); public const string SHORT_NAME = "taiko"; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 2233658428..0c7495aa52 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -5,10 +5,8 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Taiko.Replays; using osu.Framework.Input; @@ -40,8 +38,6 @@ namespace osu.Game.Rulesets.Taiko.UI new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); } - public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(Beatmap); - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer(); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 45aa904b98..cfe04fc6d1 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; @@ -62,6 +63,12 @@ namespace osu.Game.Rulesets /// public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null); + /// + /// Creates a for a beatmap converted to this ruleset. + /// + /// The score processor. + public virtual ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ScoreProcessor(beatmap); + /// /// Creates a to convert a to one that is applicable for this . /// diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5033fd0686..4ff8ce9e0a 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -305,8 +305,6 @@ namespace osu.Game.Rulesets.UI /// The Playfield. protected abstract Playfield CreatePlayfield(); - public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(Beatmap); - /// /// Applies the active mods to this DrawableRuleset. /// @@ -475,13 +473,6 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); - - /// - /// Create a for the associated ruleset and link with this - /// . - /// - /// A score processor. - public abstract ScoreProcessor CreateScoreProcessor(); } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index fa320e9a4f..5dfdeb5ebc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Play DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); - ScoreProcessor = DrawableRuleset.CreateScoreProcessor(); + ScoreProcessor = ruleset.CreateScoreProcessor(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); if (!ScoreProcessor.Mode.Disabled) From 35276c37399e9fbfcc0b16c88eb4448760e16470 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 21:26:23 +0900 Subject: [PATCH 137/189] Prevent test scene failures through casting softly --- .../Objects/Drawables/DrawableSlider.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 03183beff1..cd3c572ba0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SliderBall Ball; public readonly SkinnableDrawable Body; - private PlaySliderBody sliderBody => (PlaySliderBody)Body.Drawable; + private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; private readonly Container headContainer; private readonly Container tailContainer; @@ -154,16 +154,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); Ball.UpdateProgress(completionProgress); - sliderBody.UpdateProgress(completionProgress); + sliderBody?.UpdateProgress(completionProgress); foreach (DrawableHitObject hitObject in NestedHitObjects) { - if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(sliderBody.SnakedStart ?? 0), slider.Path.PositionAt(sliderBody.SnakedEnd ?? 0)); + if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(sliderBody?.SnakedStart ?? 0), slider.Path.PositionAt(sliderBody?.SnakedEnd ?? 0)); if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking; } - Size = sliderBody.Size; - OriginPosition = sliderBody.PathOffset; + Size = sliderBody?.Size ?? Vector2.Zero; + OriginPosition = sliderBody?.PathOffset ?? Vector2.Zero; if (DrawSize != Vector2.Zero) { @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override void OnKilled() { base.OnKilled(); - sliderBody.RecyclePath(); + sliderBody?.RecyclePath(); } protected override void ApplySkin(ISkinSource skin, bool allowFallback) @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); private class DefaultSliderBody : PlaySliderBody { From 69da6ed9a16438aa6b9fe034ee127879b4d66c00 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Dec 2019 23:53:18 +0900 Subject: [PATCH 138/189] Fix test re-using the same storage --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index a95e699470..a139c3a8c2 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestOnlineScoreIsAvailableLocally() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestOnlineScoreIsAvailableLocally")) { try { From bcc19e29f2c2d41621025c9708cf976fce370a55 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 02:56:29 +0900 Subject: [PATCH 139/189] Fix editor crashing after re-ordering objects --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 36 ++++++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 9485189433..b7508a82f3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -14,13 +15,42 @@ namespace osu.Game.Rulesets.UI public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); + private readonly Dictionary bindable, double timeAtAdd)> startTimeMap = new Dictionary, double)>(); + public HitObjectContainer() { RelativeSizeAxes = Axes.Both; } - public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject); - public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject); + public virtual void Add(DrawableHitObject hitObject) + { + // Added first for the comparer to remain ordered during AddInternal + startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime); + startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); + + AddInternal(hitObject); + } + + public virtual bool Remove(DrawableHitObject hitObject) + { + bool result = RemoveInternal(hitObject); + + // Removed last for the comparer to remain ordered during RemoveInternal + startTimeMap[hitObject].bindable.UnbindAll(); + startTimeMap.Remove(hitObject); + + return result; + } + + private void onStartTimeChanged(DrawableHitObject hitObject) + { + if (!RemoveInternal(hitObject)) + return; + + // Update the stored time, preserving the existing bindable + startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime); + AddInternal(hitObject); + } protected override int Compare(Drawable x, Drawable y) { @@ -28,7 +58,7 @@ namespace osu.Game.Rulesets.UI return base.Compare(x, y); // Put earlier hitobjects towards the end of the list, so they handle input first - int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd); return i == 0 ? CompareReverseChildID(x, y) : i; } From 0a278ef9432d06ab6ae6f94be332296c5eba9e6f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 14 Dec 2019 11:40:59 +0100 Subject: [PATCH 140/189] Apply review suggestions --- osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs | 2 +- osu.Game/Overlays/News/NewsArticleCover.cs | 6 ++---- osu.Game/Overlays/NewsOverlay.cs | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index f870a12fc3..a8e1f40ecf 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online private class TestNewsOverlay : NewsOverlay { - public new void LoadAndShowChild(NewsContent content) => base.LoadAndShowChild(content); + public new void LoadAndShowChild(NewsContent content) => base.LoadAndShowContent(content); } private class NewsCoverTest : NewsContent diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index e2485bd170..e484309a18 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -90,7 +90,6 @@ namespace osu.Game.Overlays.News bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); } - //news article cover background [LongRunningLoad] private class NewsBackground : Sprite { @@ -108,7 +107,6 @@ namespace osu.Game.Overlays.News } } - //date container private class DateContainer : Container, IHasTooltip { private readonly DateTime date; @@ -134,7 +132,7 @@ namespace osu.Game.Overlays.News Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), - Text = date.ToString("dd MMM yyy"), + Text = date.ToString("d MMM yyy").ToUpper(), Margin = new MarginPadding { Vertical = 4, @@ -144,7 +142,7 @@ namespace osu.Game.Overlays.News }; } - public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz"); + public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper(); } //fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index db4b118bf8..e7471cb21d 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -59,19 +59,19 @@ namespace osu.Game.Overlays Current.TriggerChange(); } - private CancellationTokenSource loadChildCancellation; + private CancellationTokenSource loadContentCancellation; - protected void LoadAndShowChild(NewsContent newContent) + protected void LoadAndShowContent(NewsContent newContent) { content.FadeTo(0.2f, 300, Easing.OutQuint); - loadChildCancellation?.Cancel(); + loadContentCancellation?.Cancel(); LoadComponentAsync(newContent, c => { content.Child = c; content.FadeIn(300, Easing.OutQuint); - }, (loadChildCancellation = new CancellationTokenSource()).Token); + }, (loadContentCancellation = new CancellationTokenSource()).Token); } public void ShowFrontPage() From ad7923f9b9edf87dfd6bd1acedaa44fad9f234be Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 17 Dec 2019 19:25:17 +0100 Subject: [PATCH 141/189] Fix test methods not being renamed. --- osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index a8e1f40ecf..d47c972564 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -22,12 +22,12 @@ namespace osu.Game.Tests.Visual.Online AddStep(@"Show front page", () => news.ShowFrontPage()); AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); - AddStep(@"Article covers", () => news.LoadAndShowChild(new NewsCoverTest())); + AddStep(@"Article covers", () => news.LoadAndShowContent(new NewsCoverTest())); } private class TestNewsOverlay : NewsOverlay { - public new void LoadAndShowChild(NewsContent content) => base.LoadAndShowContent(content); + public new void LoadAndShowContent(NewsContent content) => base.LoadAndShowContent(content); } private class NewsCoverTest : NewsContent From 7e58b4a948c1d4fb3b1b50e4df01b315917afe77 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 18 Dec 2019 03:03:12 +0300 Subject: [PATCH 142/189] Early-return on potential division by zero in SPM calculation --- .../Objects/Drawables/Pieces/SpinnerSpmCounter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs index b1d90c49f6..97a7b98c5b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -62,6 +63,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void SetRotation(float currentRotation) { + // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. + if (Precision.AlmostEquals(0, Time.Elapsed)) + return; + // If we've gone back in time, it's fine to work with a fresh set of records for now if (records.Count > 0 && Time.Current < records.Last().Time) records.Clear(); @@ -71,6 +76,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var record = records.Peek(); while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration) record = records.Dequeue(); + SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; } From cbfbbf9999be5dfb60a36ebad95c9d29f292b8f8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 18 Dec 2019 03:04:37 +0300 Subject: [PATCH 143/189] Make SpmCounter public --- .../Objects/Drawables/DrawableSpinner.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1261d3d19a..de11ab6419 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; - private readonly SpinnerSpmCounter spmCounter; + public readonly SpinnerSpmCounter SpmCounter; private readonly Container mainContainer; @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, } }, - spmCounter = new SpinnerSpmCounter + SpmCounter = new SpinnerSpmCounter { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -177,8 +177,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!spmCounter.IsPresent && Disc.Tracking) - spmCounter.FadeIn(HitObject.TimeFadeIn); + if (!SpmCounter.IsPresent && Disc.Tracking) + SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); } @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; - spmCounter.SetRotation(Disc.RotationAbsolute); + SpmCounter.SetRotation(Disc.RotationAbsolute); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); From 12a02cf6d927e583c6cba57142beda24f7bef041 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 18 Dec 2019 03:07:06 +0300 Subject: [PATCH 144/189] Extend spinner duration a bit To allow the times sought to be in the spinner time range --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index d0ce0c33c2..ea42251a69 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Spinner { Position = new Vector2(256, 192), - EndTime = 5000, + EndTime = 6000, }, // placeholder object to avoid hitting the results screen new HitObject From f359a79b7ec5d12b9ec8aa2b71e31c86039ab2ad Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 18 Dec 2019 03:08:05 +0300 Subject: [PATCH 145/189] Add test ensuring correct SPM calculation on rewinding --- .../TestSceneSpinnerRotation.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index ea42251a69..2eda555018 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -70,6 +70,19 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100)); } + [Test] + public void TestSpinPerMinuteOnRewind() + { + double estimatedSpm = 0; + + addSeekStep(2500); + AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); + + addSeekStep(5000); + addSeekStep(2500); + AddAssert("is spm almost same", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + } + private void addSeekStep(double time) { AddStep($"seek to {time}", () => track.Seek(time)); From df8f8ffd0d3fc699b9a0dee2e3aa698231227afe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 12:03:15 +0900 Subject: [PATCH 146/189] Fix potential exception during removal --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index b7508a82f3..1e33b15184 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -33,13 +33,14 @@ namespace osu.Game.Rulesets.UI public virtual bool Remove(DrawableHitObject hitObject) { - bool result = RemoveInternal(hitObject); + if (!RemoveInternal(hitObject)) + return false; // Removed last for the comparer to remain ordered during RemoveInternal startTimeMap[hitObject].bindable.UnbindAll(); startTimeMap.Remove(hitObject); - return result; + return true; } private void onStartTimeChanged(DrawableHitObject hitObject) From 0f5ef78b69c552545e5cc7dfc9b6a91bd2afafb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 12:39:50 +0900 Subject: [PATCH 147/189] Update client id --- osu.Desktop/DiscordRichPresenceClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs index af5b42b275..7a661fe6a2 100644 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ b/osu.Desktop/DiscordRichPresenceClient.cs @@ -16,7 +16,7 @@ namespace osu.Desktop { internal class DiscordRichPresenceClient : Component { - private const string client_id = "563024054391537674"; + private const string client_id = "367827983903490050"; private Bindable user; From 1fe0e45a9cf4e3c1de1deab229aadce365008d26 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 13:37:37 +0900 Subject: [PATCH 148/189] Implement legacy slider border shadow --- .../Objects/Drawables/Pieces/PlaySliderBody.cs | 7 +------ osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 14 +++++++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs index aa9caf193e..cedf2f6e09 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs @@ -24,16 +24,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private OsuRulesetConfigManager config { get; set; } private Slider slider; - private float defaultPathRadius; [BackgroundDependencyLoader] private void load(ISkinSource skin) { slider = (Slider)drawableObject.HitObject; - defaultPathRadius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; scaleBindable = slider.ScaleBindable.GetBoundCopy(); - scaleBindable.BindValueChanged(_ => updatePathRadius(), true); + scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true); pathVersion = slider.Path.Version.GetBoundCopy(); pathVersion.BindValueChanged(_ => Refresh()); @@ -48,9 +46,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; } - private void updatePathRadius() - => PathRadius = defaultPathRadius * scaleBindable.Value; - private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour) => AccentColour = skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? defaultAccentColour; } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index dea08f843e..8fc07d2c9d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -15,19 +15,27 @@ namespace osu.Game.Rulesets.Osu.Skinning private class LegacyDrawableSliderPath : DrawableSliderPath { + private const float shadow_portion = 0.06f; + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) { - if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion) + float realBorderPortion = shadow_portion + CalculatedBorderPortion; + float realGradientPortion = 1 - realBorderPortion; + + if (position <= shadow_portion) + return new Color4(0f, 0f, 0f, 0.25f * position / shadow_portion); + + if (position <= realBorderPortion) return BorderColour; - position -= BORDER_PORTION; + position -= realBorderPortion; Color4 outerColour = AccentColour.Darken(0.1f); Color4 innerColour = lighten(AccentColour, 0.5f); - return Interpolation.ValueAt(position / GRADIENT_PORTION, outerColour, innerColour, 0, 1); + return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1); } /// From 756d847ad8c848ac24800fd4436ef31fbf261d59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:07:03 +0900 Subject: [PATCH 149/189] Fix user not getting an initial status --- osu.Game/Online/API/APIAccess.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8bfc28e774..23c931d161 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -153,6 +153,10 @@ namespace osu.Game.Online.API userReq.Success += u => { LocalUser.Value = u; + + // todo: save/pull from settings + LocalUser.Value.Status.Value = new UserStatusOnline(); + failureCount = 0; //we're connected! From 2f5b27e97c11d122be488e2dbb4ff5dee349e4e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:07:12 +0900 Subject: [PATCH 150/189] Make user bindables readonly --- osu.Game/Users/User.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index ebd9dbecd1..5d0ffd5a67 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -26,9 +26,9 @@ namespace osu.Game.Users [JsonProperty(@"country")] public Country Country; - public Bindable Status = new Bindable(); + public readonly Bindable Status = new Bindable(); - public IBindable Activity = new Bindable(); + public readonly Bindable Activity = new Bindable(); //public Team Team; From f53fd6e4bcfce9b22eb692c223bdf0cb30332e09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:07:21 +0900 Subject: [PATCH 151/189] Fix status capitalisation --- osu.Game/Users/UserActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 918c547978..8030fc55a2 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -62,7 +62,7 @@ namespace osu.Game.Users public class InLobby : UserActivity { - public override string Status => @"In a Multiplayer Lobby"; + public override string Status => @"In a multiplayer lobby"; } } } From 0a3d339dd91a6d18f23e1bf7097aec50fe8a65a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:07:32 +0900 Subject: [PATCH 152/189] Load discord RPC asynchronously --- osu.Desktop/OsuGameDesktop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 9f2a4b12a1..f70cc24159 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -61,7 +61,7 @@ namespace osu.Desktop Add(new SimpleUpdateManager()); } - Add(new DiscordRichPresenceClient()); + LoadComponentAsync(new DiscordRichPresence(), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) From b65847b0d76bd3fffb629176e0a33cdb34764c40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:07:53 +0900 Subject: [PATCH 153/189] Refactor / rewrite discord code --- osu.Desktop/DiscordRichPresence.cs | 120 +++++++++++++++++++++++ osu.Desktop/DiscordRichPresenceClient.cs | 104 -------------------- 2 files changed, 120 insertions(+), 104 deletions(-) create mode 100644 osu.Desktop/DiscordRichPresence.cs delete mode 100644 osu.Desktop/DiscordRichPresenceClient.cs diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs new file mode 100644 index 0000000000..d1bd8fd292 --- /dev/null +++ b/osu.Desktop/DiscordRichPresence.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using DiscordRPC; +using DiscordRPC.Message; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Users; +using LogLevel = osu.Framework.Logging.LogLevel; +using User = osu.Game.Users.User; + +namespace osu.Desktop +{ + internal class DiscordRichPresence : Component + { + private const string client_id = "367827983903490050"; + + private DiscordRpcClient client; + + [Resolved] + private IBindable ruleset { get; set; } + + private Bindable user; + + private readonly IBindable status = new Bindable(); + private readonly IBindable activity = new Bindable(); + + private readonly RichPresence presence = new RichPresence + { + Assets = new Assets { LargeImageKey = "osu_logo_lazer", } + }; + + [BackgroundDependencyLoader] + private void load(IAPIProvider provider) + { + client = new DiscordRpcClient(client_id) + { + SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. + }; + + client.OnReady += onReady; + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); + client.OnConnectionFailed += (_, e) => Logger.Log($"An connection occurred with Discord RPC Client: {e.Type}", LoggingTarget.Network, LogLevel.Error); + + (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u => + { + status.UnbindBindings(); + status.BindTo(u.NewValue.Status); + + activity.UnbindBindings(); + activity.BindTo(u.NewValue.Activity); + }, true); + + ruleset.BindValueChanged(_ => updateStatus()); + status.BindValueChanged(_ => updateStatus()); + activity.BindValueChanged(_ => updateStatus()); + + client.Initialize(); + } + + private void onReady(object _, ReadyMessage __) + { + Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); + updateStatus(); + } + + private void updateStatus() + { + if (status.Value is UserStatusOffline) + { + client.ClearPresence(); + return; + } + + if (status.Value is UserStatusOnline && activity.Value != null) + { + presence.State = activity.Value.Status; + presence.Details = getDetails(activity.Value); + } + else + { + presence.State = "Idle"; + presence.Details = string.Empty; + } + + // update user information + presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); + + // update ruleset + presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_unknown"; + presence.Assets.SmallImageText = ruleset.Value.Name; + + client.SetPresence(presence); + } + + private string getDetails(UserActivity activity) + { + switch (activity) + { + case UserActivity.SoloGame solo: + return solo.Beatmap.ToString(); + + case UserActivity.Editing edit: + return edit.Beatmap.ToString(); + } + + return string.Empty; + } + + protected override void Dispose(bool isDisposing) + { + client.Dispose(); + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Desktop/DiscordRichPresenceClient.cs b/osu.Desktop/DiscordRichPresenceClient.cs deleted file mode 100644 index 7a661fe6a2..0000000000 --- a/osu.Desktop/DiscordRichPresenceClient.cs +++ /dev/null @@ -1,104 +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 DiscordRPC; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Logging; -using osu.Game.Online.API; -using osu.Game.Rulesets; -using osu.Game.Users; -using static osu.Game.Users.UserActivity; -using User = osu.Game.Users.User; - -namespace osu.Desktop -{ - internal class DiscordRichPresenceClient : Component - { - private const string client_id = "367827983903490050"; - - private Bindable user; - - private readonly DiscordRpcClient client = new DiscordRpcClient(client_id); - - [BackgroundDependencyLoader] - private void load(IAPIProvider provider) - { - user = provider.LocalUser.GetBoundCopy(); - - user.ValueChanged += usr => - { - usr.NewValue.Activity.ValueChanged += activity => updateStatus(user.Value.Status.Value, activity.NewValue); - usr.NewValue.Status.ValueChanged += status => updateStatus(status.NewValue, user.Value.Activity.Value); - }; - - user.TriggerChange(); - - enableLogging(); - - client.Initialize(); - } - - private void enableLogging() - { - client.OnReady += (_, __) => Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client : {e.Message}", LoggingTarget.Network, LogLevel.Debug); - client.OnConnectionFailed += (_, e) => Logger.Log("Discord RPC Client failed to initialize : is discord running ?", LoggingTarget.Network, LogLevel.Debug); - client.OnPresenceUpdate += (_, __) => Logger.Log("Updated Discord Rich Presence", LoggingTarget.Network, LogLevel.Debug); - } - - private void updateStatus(UserStatus st, UserActivity a) - { - var presence = defaultPresence(st is UserStatusOnline ? a?.Status ?? st.Message : st.Message); //display the current user activity if the user status is online & user activity != null, else display the current user online status - - if (!(st is UserStatusOnline)) //don't update the presence any further if the current user status is DND / Offline & simply return with the default presence - { - client.SetPresence(presence); - return; - } - - switch (a) - { - case SoloGame game: - presence.State = $"{game.Beatmap.Metadata.Artist} - {game.Beatmap.Metadata.Title} [{game.Beatmap.Version}]"; - setPresenceGamemode(game.Ruleset, presence); - break; - - case Editing editing: - presence.State = $"{editing.Beatmap.Metadata.Artist} - {editing.Beatmap.Metadata.Title} " + (!string.IsNullOrEmpty(editing.Beatmap.Version) ? $"[{editing.Beatmap.Version}]" : ""); - presence.Assets.SmallImageKey = "edit"; - presence.Assets.SmallImageText = "editing"; - break; - } - - client.SetPresence(presence); - } - - private void setPresenceGamemode(RulesetInfo ruleset, RichPresence presence) - { - if (ruleset.ID != null && ruleset.ID <= 3) //legacy rulesets use an ID between 0 and 3 - presence.Assets.SmallImageKey = ruleset.ShortName; - else - presence.Assets.SmallImageKey = "unknown"; //not a legacy ruleset so let's display the unknown ruleset icon. - - presence.Assets.SmallImageText = ruleset.ShortName; - } - - private RichPresence defaultPresence(string status) => new RichPresence - { - Details = status, - Assets = new Assets - { - LargeImageKey = "lazer", - LargeImageText = user.Value.Username - } - }; - - protected override void Dispose(bool isDisposing) - { - client.Dispose(); - base.Dispose(isDisposing); - } - } -} From 0710e5ba13749212b158f008ecc9c2b5987b3ca8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:35:18 +0900 Subject: [PATCH 154/189] Rename unknown mode assets (discord dev page broken) --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d1bd8fd292..b53ca6161b 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -91,7 +91,7 @@ namespace osu.Desktop presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); // update ruleset - presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_unknown"; + presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; client.SetPresence(presence); From 6e0802e50cfdfe0cb1010a64ca1f1a1fbaff7dba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 14:49:09 +0900 Subject: [PATCH 155/189] Remove RulesetInfo parameter from Ruleset constructor --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 5 ----- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 5 ----- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 ----- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 5 ----- .../Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs | 5 ----- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 7 +------ osu.Game/Rulesets/Ruleset.cs | 4 ++-- osu.Game/Rulesets/RulesetInfo.cs | 2 +- osu.Game/Rulesets/RulesetStore.cs | 4 ++-- 9 files changed, 6 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 065771bc4a..b8a844cb86 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -137,10 +137,5 @@ namespace osu.Game.Rulesets.Catch public override int? LegacyID => 2; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); - - public CatchRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 8b53ce01f6..bf630cf892 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -186,11 +186,6 @@ namespace osu.Game.Rulesets.Mania public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this); - public ManiaRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } - public override IEnumerable AvailableVariants { get diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 835ae2564c..27af615935 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -183,10 +183,5 @@ namespace osu.Game.Rulesets.Osu public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - - public OsuRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 4d6c5fa1c0..ca7ab30867 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -136,10 +136,5 @@ namespace osu.Game.Rulesets.Taiko public override int? LegacyID => 1; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - - public TaikoRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index c958932730..ae20bbc86d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -194,11 +194,6 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestScrollingRuleset : Ruleset { - public TestScrollingRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } - public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new TestDrawableScrollingRuleset(this, beatmap, mods); diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 9ea254b23f..46efe38d37 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps private class DummyRulesetInfo : RulesetInfo { - public override Ruleset CreateInstance() => new DummyRuleset(this); + public override Ruleset CreateInstance() => new DummyRuleset(); private class DummyRuleset : Ruleset { @@ -70,11 +70,6 @@ namespace osu.Game.Beatmaps public override string ShortName => "dummy"; - public DummyRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } - private class DummyBeatmapConverter : IBeatmapConverter { public event Action> ObjectConverted; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 6f5fe066aa..7ad93379f0 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -49,9 +49,9 @@ namespace osu.Game.Rulesets public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null; - protected Ruleset(RulesetInfo rulesetInfo = null) + protected Ruleset() { - RulesetInfo = rulesetInfo ?? createRulesetInfo(); + RulesetInfo = createRulesetInfo(); } /// diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 6a69fd8dd0..d695e0b56d 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets { if (!Available) return null; - return (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this); + return (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo)); } public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 7d13afe9e5..5d0c5c7ccf 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets { var context = usage.Context; - var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); + var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList(); //add all legacy modes in correct order foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID)) @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets // this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version). asm.Version = null; return Assembly.Load(asm); - }, null), (RulesetInfo)null)).RulesetInfo; + }, null))).RulesetInfo; r.Name = instanceInfo.Name; r.ShortName = instanceInfo.ShortName; From 826b271371188a64d9e9e6660fbe2d23af254ca6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 15:39:36 +0900 Subject: [PATCH 156/189] Use exact ratio of legacy to default object size --- osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 3 ++- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 8fc07d2c9d..d41135ca69 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.MathUtils; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osuTK.Graphics; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private class LegacyDrawableSliderPath : DrawableSliderPath { - private const float shadow_portion = 0.06f; + private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 71770dedce..266b619334 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Skinning /// Their hittable area is 128px, but the actual circle portion is 118px. /// We must account for some gameplay elements such as slider bodies, where this padding is not present. /// - private const float legacy_circle_radius = 64 - 5; + public const float LEGACY_CIRCLE_RADIUS = 64 - 5; public OsuLegacySkinTransformer(ISkinSource source) { @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { case OsuSkinConfiguration.SliderPathRadius: if (hasHitCircle.Value) - return SkinUtils.As(new BindableFloat(legacy_circle_radius)); + return SkinUtils.As(new BindableFloat(LEGACY_CIRCLE_RADIUS)); break; } From d4a4efb734f8799337525c767d01e3825083d897 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 16:07:14 +0900 Subject: [PATCH 157/189] Tidy up test --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 2eda555018..02ce77e707 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -79,8 +79,10 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); addSeekStep(5000); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + addSeekStep(2500); - AddAssert("is spm almost same", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); } private void addSeekStep(double time) From 54572b6de9e685e96168ff36263103437684ede9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 16:42:18 +0900 Subject: [PATCH 158/189] Update selected mdos references --- .../Visual/SongSelect/TestSceneBeatmapDetailArea.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index d4805a73e4..66144cbfe4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.SongSelect Position = new Vector2(0, 25), }); - modDisplay.Current.BindTo(Mods); + modDisplay.Current.BindTo(SelectedMods); AddStep("all metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { @@ -204,7 +204,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }; + SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }; }); AddStep("with HR mod", () => @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual.SongSelect } }); - Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }; + SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }; }); } } From e87aa281bfde25de44342ceed4ac21c7309c1c14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 17:00:35 +0900 Subject: [PATCH 159/189] Don't clone beatmap unnecessarily --- .../Screens/Select/Details/AdvancedStats.cs | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 3e7d4e244b..529d63b475 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -68,41 +68,39 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - BeatmapInfo processed = Beatmap?.Clone(); + var baseDifficulty = Beatmap?.BaseDifficulty; + var adjustedDifficulty = baseDifficulty; - if (processed != null && mods.Value.Any(m => m is IApplicableToDifficulty)) + if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) { - processed.BaseDifficulty = processed.BaseDifficulty.Clone(); + adjustedDifficulty = adjustedDifficulty?.Clone(); foreach (var mod in mods.Value.OfType()) - mod.ApplyToDifficulty(processed.BaseDifficulty); + mod.ApplyToDifficulty(adjustedDifficulty); } - BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; - BeatmapDifficulty moddedDifficulty = processed?.BaseDifficulty; - //mania specific - if ((processed?.Ruleset?.ID ?? 0) == 3) + if ((Beatmap?.Ruleset?.ID ?? 0) == 3) { firstValue.Title = "Key Amount"; firstValue.BaseValue = (int)MathF.Round(baseDifficulty?.CircleSize ?? 0); - firstValue.ModdedValue = (int)MathF.Round(moddedDifficulty?.CircleSize ?? 0); + firstValue.ModdedValue = (int)MathF.Round(adjustedDifficulty?.CircleSize ?? 0); } else { firstValue.Title = "Circle Size"; firstValue.BaseValue = baseDifficulty?.CircleSize ?? 0; - firstValue.ModdedValue = moddedDifficulty?.CircleSize ?? 0; + firstValue.ModdedValue = adjustedDifficulty?.CircleSize ?? 0; } hpDrain.BaseValue = baseDifficulty?.DrainRate ?? 0; accuracy.BaseValue = baseDifficulty?.OverallDifficulty ?? 0; approachRate.BaseValue = baseDifficulty?.ApproachRate ?? 0; - starDifficulty.BaseValue = (float)(processed?.StarDifficulty ?? 0); + starDifficulty.BaseValue = (float)(Beatmap?.StarDifficulty ?? 0); - hpDrain.ModdedValue = moddedDifficulty?.DrainRate ?? 0; - accuracy.ModdedValue = moddedDifficulty?.OverallDifficulty ?? 0; - approachRate.ModdedValue = moddedDifficulty?.ApproachRate ?? 0; + hpDrain.ModdedValue = adjustedDifficulty?.DrainRate ?? 0; + accuracy.ModdedValue = adjustedDifficulty?.OverallDifficulty ?? 0; + approachRate.ModdedValue = adjustedDifficulty?.ApproachRate ?? 0; } private class StatisticRow : Container, IHasAccentColour From ada2ae2b2c9e279b4ab4922be136c42e87d7205b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 17:12:41 +0900 Subject: [PATCH 160/189] Use tuple to avoid potential for incorrect display --- .../Screens/Select/Details/AdvancedStats.cs | 66 ++++++++----------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 529d63b475..f684238a38 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -68,12 +68,12 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - var baseDifficulty = Beatmap?.BaseDifficulty; - var adjustedDifficulty = baseDifficulty; + BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + BeatmapDifficulty adjustedDifficulty = null; if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) { - adjustedDifficulty = adjustedDifficulty?.Clone(); + adjustedDifficulty = baseDifficulty.Clone(); foreach (var mod in mods.Value.OfType()) mod.ApplyToDifficulty(adjustedDifficulty); @@ -83,24 +83,19 @@ namespace osu.Game.Screens.Select.Details if ((Beatmap?.Ruleset?.ID ?? 0) == 3) { firstValue.Title = "Key Amount"; - firstValue.BaseValue = (int)MathF.Round(baseDifficulty?.CircleSize ?? 0); - firstValue.ModdedValue = (int)MathF.Round(adjustedDifficulty?.CircleSize ?? 0); + firstValue.Value = ((int)MathF.Round(baseDifficulty?.CircleSize ?? 0), (int)MathF.Round(adjustedDifficulty?.CircleSize ?? 0)); } else { firstValue.Title = "Circle Size"; - firstValue.BaseValue = baseDifficulty?.CircleSize ?? 0; - firstValue.ModdedValue = adjustedDifficulty?.CircleSize ?? 0; + firstValue.Value = (baseDifficulty?.CircleSize ?? 0, adjustedDifficulty?.CircleSize); } - hpDrain.BaseValue = baseDifficulty?.DrainRate ?? 0; - accuracy.BaseValue = baseDifficulty?.OverallDifficulty ?? 0; - approachRate.BaseValue = baseDifficulty?.ApproachRate ?? 0; - starDifficulty.BaseValue = (float)(Beatmap?.StarDifficulty ?? 0); + starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null); - hpDrain.ModdedValue = adjustedDifficulty?.DrainRate ?? 0; - accuracy.ModdedValue = adjustedDifficulty?.OverallDifficulty ?? 0; - approachRate.ModdedValue = adjustedDifficulty?.ApproachRate ?? 0; + hpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate); + accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty); + approachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate); } private class StatisticRow : Container, IHasAccentColour @@ -110,7 +105,7 @@ namespace osu.Game.Screens.Select.Details private readonly float maxValue; private readonly bool forceDecimalPlaces; - private readonly OsuSpriteText name, value; + private readonly OsuSpriteText name, valueText; private readonly Bar bar, modBar; [Resolved] @@ -122,36 +117,29 @@ namespace osu.Game.Screens.Select.Details set => name.Text = value; } - private float baseValue; + private (float baseValue, float? adjustedValue) value; - private float moddedValue; - - public float BaseValue + public (float baseValue, float? adjustedValue) Value { - get => baseValue; + get => value; set { - baseValue = value; - bar.Length = value / maxValue; - this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); - } - } + if (value == this.value) + return; - public float ModdedValue - { - get => moddedValue; - set - { - moddedValue = value; - modBar.Length = value / maxValue; - this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); + this.value = value; - if (moddedValue > baseValue) - modBar.AccentColour = this.value.Colour = colours.Red; - else if (moddedValue < baseValue) - modBar.AccentColour = this.value.Colour = colours.BlueDark; + bar.Length = value.baseValue / maxValue; + + valueText.Text = (value.adjustedValue ?? value.baseValue).ToString(forceDecimalPlaces ? "0.00" : "0.##"); + modBar.Length = (value.adjustedValue ?? 0) / maxValue; + + if (value.adjustedValue > value.baseValue) + modBar.AccentColour = valueText.Colour = colours.Red; + else if (value.adjustedValue < value.baseValue) + modBar.AccentColour = valueText.Colour = colours.BlueDark; else - modBar.AccentColour = this.value.Colour = Color4.White; + modBar.AccentColour = valueText.Colour = Color4.White; } } @@ -203,7 +191,7 @@ namespace osu.Game.Screens.Select.Details Origin = Anchor.TopRight, Width = value_width, RelativeSizeAxes = Axes.Y, - Child = value = new OsuSpriteText + Child = valueText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 602ce698d5633a3b655682a1b94d952a4adc99ee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 17:21:38 +0900 Subject: [PATCH 161/189] Fix storyboard vectorscale and scale cross-pollution --- .../Formats/LegacyStoryboardDecoder.cs | 4 +-- osu.Game/Storyboards/CommandTimelineGroup.cs | 3 ++- .../Drawables/DrawableStoryboardAnimation.cs | 27 +++++++++++++++++-- .../Drawables/DrawableStoryboardSprite.cs | 27 +++++++++++++++++-- osu.Game/Storyboards/Drawables/IFlippable.cs | 8 +++--- .../Storyboards/Drawables/IVectorScalable.cs | 21 +++++++++++++++ osu.Game/Storyboards/StoryboardSprite.cs | 20 ++++++++++---- 7 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Storyboards/Drawables/IVectorScalable.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index ccd46ab559..756aa45eed 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -176,7 +176,7 @@ namespace osu.Game.Beatmaps.Formats { var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); + timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue); break; } @@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Formats var startY = float.Parse(split[5], CultureInfo.InvariantCulture); var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); + timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); break; } diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 364c971874..7b6e667d4f 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -16,7 +16,8 @@ namespace osu.Game.Storyboards { public CommandTimeline X = new CommandTimeline(); public CommandTimeline Y = new CommandTimeline(); - public CommandTimeline Scale = new CommandTimeline(); + public CommandTimeline Scale = new CommandTimeline(); + public CommandTimeline VectorScale = new CommandTimeline(); public CommandTimeline Rotation = new CommandTimeline(); public CommandTimeline Colour = new CommandTimeline(); public CommandTimeline Alpha = new CommandTimeline(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 4f8e39fa1b..a452c7540d 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -8,21 +8,44 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardAnimation : TextureAnimation, IFlippable + public class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable { public StoryboardAnimation Animation { get; private set; } public bool FlipH { get; set; } public bool FlipV { get; set; } + private Vector2 vectorScale = Vector2.One; + + public Vector2 VectorScale + { + get => vectorScale; + set + { + if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) + value.X = Precision.FLOAT_EPSILON; + if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) + value.Y = Precision.FLOAT_EPSILON; + + if (vectorScale == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(VectorScale)} must be finite, but is {value}."); + + vectorScale = value; + Invalidate(Invalidation.MiscGeometry); + } + } + public override bool RemoveWhenNotAlive => false; protected override Vector2 DrawScale - => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; public override Anchor Origin { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ff48dab7e5..42c7cfacb2 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -8,21 +8,44 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSprite : Sprite, IFlippable + public class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable { public StoryboardSprite Sprite { get; private set; } public bool FlipH { get; set; } public bool FlipV { get; set; } + private Vector2 vectorScale = Vector2.One; + + public Vector2 VectorScale + { + get => vectorScale; + set + { + if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) + value.X = Precision.FLOAT_EPSILON; + if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) + value.Y = Precision.FLOAT_EPSILON; + + if (vectorScale == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(VectorScale)} must be finite, but is {value}."); + + vectorScale = value; + Invalidate(Invalidation.MiscGeometry); + } + } + public override bool RemoveWhenNotAlive => false; protected override Vector2 DrawScale - => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; public override Anchor Origin { diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs index 1c4cdde22d..165b3d97cc 100644 --- a/osu.Game/Storyboards/Drawables/IFlippable.cs +++ b/osu.Game/Storyboards/Drawables/IFlippable.cs @@ -6,13 +6,13 @@ using osu.Framework.Graphics.Transforms; namespace osu.Game.Storyboards.Drawables { - public interface IFlippable : ITransformable + internal interface IFlippable : ITransformable { bool FlipH { get; set; } bool FlipV { get; set; } } - public class TransformFlipH : Transform + internal class TransformFlipH : Transform { private bool valueAt(double time) => time < EndTime ? StartValue : EndValue; @@ -23,7 +23,7 @@ namespace osu.Game.Storyboards.Drawables protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipH; } - public class TransformFlipV : Transform + internal class TransformFlipV : Transform { private bool valueAt(double time) => time < EndTime ? StartValue : EndValue; @@ -34,7 +34,7 @@ namespace osu.Game.Storyboards.Drawables protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipV; } - public static class FlippableExtensions + internal static class FlippableExtensions { /// /// Adjusts after a delay. diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs new file mode 100644 index 0000000000..fcc407d460 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/IVectorScalable.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.Graphics; +using osu.Framework.Graphics.Transforms; +using osuTK; + +namespace osu.Game.Storyboards.Drawables +{ + internal interface IVectorScalable : ITransformable + { + Vector2 VectorScale { get; set; } + } + + internal static class VectorScalableExtensions + { + public static TransformSequence VectorScaleTo(this T target, Vector2 newVectorScale, double duration = 0, Easing easing = Easing.None) + where T : class, IVectorScalable + => target.TransformTo(nameof(IVectorScalable.VectorScale), newVectorScale, duration, easing); + } +} diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index d5e69fd103..abf9f58804 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -65,20 +65,30 @@ namespace osu.Game.Storyboards { applyCommands(drawable, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value, (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), false); + applyCommands(drawable, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), + false); + + if (drawable is IVectorScalable vectorScalable) + { + applyCommands(drawable, getCommands(g => g.VectorScale, triggeredGroups), (d, value) => vectorScalable.VectorScale = value, + (d, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing)); + } if (drawable is IFlippable flippable) { - applyCommands(drawable, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), false); - applyCommands(drawable, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), false); + applyCommands(drawable, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), + false); + applyCommands(drawable, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), + false); } } - private void applyCommands(Drawable drawable, IEnumerable.TypedCommand> commands, DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, bool alwaysInitialize = true) + private void applyCommands(Drawable drawable, IEnumerable.TypedCommand> commands, DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, + bool alwaysInitialize = true) where T : struct { var initialized = false; From 5aca523d353f46881df089c698275fdb9344d90e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 17:27:13 +0900 Subject: [PATCH 162/189] Fix storyboard flipping potentially not having an effect --- .../Drawables/DrawableStoryboardAnimation.cs | 31 +++++++++++++++++-- .../Drawables/DrawableStoryboardSprite.cs | 31 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 4f8e39fa1b..66c6d179b8 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -16,8 +16,35 @@ namespace osu.Game.Storyboards.Drawables { public StoryboardAnimation Animation { get; private set; } - public bool FlipH { get; set; } - public bool FlipV { get; set; } + private bool flipH; + + public bool FlipH + { + get => flipH; + set + { + if (flipH == value) + return; + + flipH = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private bool flipV; + + public bool FlipV + { + get => flipV; + set + { + if (flipV == value) + return; + + flipV = value; + Invalidate(Invalidation.MiscGeometry); + } + } public override bool RemoveWhenNotAlive => false; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ff48dab7e5..aa60096661 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -16,8 +16,35 @@ namespace osu.Game.Storyboards.Drawables { public StoryboardSprite Sprite { get; private set; } - public bool FlipH { get; set; } - public bool FlipV { get; set; } + private bool flipH; + + public bool FlipH + { + get => flipH; + set + { + if (flipH == value) + return; + + flipH = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private bool flipV; + + public bool FlipV + { + get => flipV; + set + { + if (flipV == value) + return; + + flipV = value; + Invalidate(Invalidation.MiscGeometry); + } + } public override bool RemoveWhenNotAlive => false; From 4befabc2573072c06130ff21903e93d94dbf6f69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 17:35:51 +0900 Subject: [PATCH 163/189] Split out complex method --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 180 ++++++++++-------- 1 file changed, 99 insertions(+), 81 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d38ff482ad..8d16278f60 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -217,26 +217,12 @@ namespace osu.Game.Beatmaps.Formats private void handleOsuHitObject(TextWriter writer, HitObject hitObject) { - var positionData = hitObject as IHasPosition; - var comboData = hitObject as IHasCombo; - - Debug.Assert(positionData != null); - Debug.Assert(comboData != null); - - LegacyHitObjectType hitObjectType = (LegacyHitObjectType)(comboData.ComboOffset << 4); - if (comboData.NewCombo) - hitObjectType |= LegacyHitObjectType.NewCombo; - if (hitObject is IHasCurve) - hitObjectType |= LegacyHitObjectType.Slider; - else if (hitObject is IHasEndTime) - hitObjectType |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo; - else - hitObjectType |= LegacyHitObjectType.Circle; + var positionData = (IHasPosition)hitObject; writer.Write(FormattableString.Invariant($"{positionData.X},")); writer.Write(FormattableString.Invariant($"{positionData.Y},")); writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); - writer.Write(FormattableString.Invariant($"{(int)hitObjectType},")); + writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); writer.Write(hitObject is IHasCurve ? FormattableString.Invariant($"0,") @@ -244,76 +230,108 @@ namespace osu.Game.Beatmaps.Formats if (hitObject is IHasCurve curveData) { - PathType? lastType = null; - - for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) - { - PathControlPoint point = curveData.Path.ControlPoints[i]; - - if (point.Type.Value != null) - { - if (point.Type.Value != lastType) - { - switch (point.Type.Value) - { - case PathType.Bezier: - writer.Write("B|"); - break; - - case PathType.Catmull: - writer.Write("C|"); - break; - - case PathType.PerfectCurve: - writer.Write("P|"); - break; - - case PathType.Linear: - writer.Write("L|"); - break; - } - - lastType = point.Type.Value; - } - else - { - // New segment with the same type - duplicate the control point - writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|")); - } - } - - if (i != 0) - { - writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); - writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); - } - } - - writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); - writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); - - for (int i = 0; i < curveData.NodeSamples.Count; i++) - { - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); - } - - for (int i = 0; i < curveData.NodeSamples.Count; i++) - { - writer.Write(getSampleBank(curveData.NodeSamples[i], true)); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); - } + addCurveData(writer, curveData, positionData); + writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); + } + else + { + if (hitObject is IHasEndTime endTimeData) + writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); + writer.Write(getSampleBank(hitObject.Samples)); } - else if (hitObject is IHasEndTime endTimeData) - writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); - - writer.Write(hitObject is IHasCurve - ? getSampleBank(hitObject.Samples, zeroBanks: true) - : getSampleBank(hitObject.Samples)); writer.WriteLine(); } + private static LegacyHitObjectType getObjectType(HitObject hitObject) + { + var comboData = (IHasCombo)hitObject; + + var type = (LegacyHitObjectType)(comboData.ComboOffset << 4); + + if (comboData.NewCombo) type |= LegacyHitObjectType.NewCombo; + + switch (hitObject) + { + case IHasCurve _: + type |= LegacyHitObjectType.Slider; + break; + + case IHasEndTime _: + type |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo; + break; + + default: + type |= LegacyHitObjectType.Circle; + break; + } + + return type; + } + + private void addCurveData(TextWriter writer, IHasCurve curveData, IHasPosition positionData) + { + PathType? lastType = null; + + for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) + { + PathControlPoint point = curveData.Path.ControlPoints[i]; + + if (point.Type.Value != null) + { + if (point.Type.Value != lastType) + { + switch (point.Type.Value) + { + case PathType.Bezier: + writer.Write("B|"); + break; + + case PathType.Catmull: + writer.Write("C|"); + break; + + case PathType.PerfectCurve: + writer.Write("P|"); + break; + + case PathType.Linear: + writer.Write("L|"); + break; + } + + lastType = point.Type.Value; + } + else + { + // New segment with the same type - duplicate the control point + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|")); + } + } + + if (i != 0) + { + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); + writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + } + } + + writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); + writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(getSampleBank(curveData.NodeSamples[i], true)); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + } + private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); From 8e651962c7d7c5dbe98bc8e801db9a3514745e7a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 17:41:30 +0900 Subject: [PATCH 164/189] Fix incorrectly binding inside BDL load() --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index f684238a38..9c9c33274f 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -63,7 +63,13 @@ namespace osu.Game.Screens.Select.Details private void load(OsuColour colours) { starDifficulty.AccentColour = colours.Yellow; - mods.ValueChanged += _ => updateStatistics(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + mods.BindValueChanged(_ => updateStatistics(), true); } private void updateStatistics() From bf85f4affb1eca46513b17dd2442b40df78cce32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 17:53:26 +0900 Subject: [PATCH 165/189] Fix editor crashing when loading a beatmap for an unsupported ruleset --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 6984716a2c..5d9757778d 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -32,6 +32,6 @@ namespace osu.Game.Screens.Edit.Compose return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } - protected override Drawable CreateTimelineContent() => new TimelineHitObjectDisplay(composer.EditorBeatmap); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(composer.EditorBeatmap); } } From d65e37d7959fe4e89a6091c53b805059dbe73935 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 17:58:29 +0900 Subject: [PATCH 166/189] Fix typo --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index b04d01004a..b9ef279f5c 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Graphics.Containers public double TimeSinceLastBeat { get; private set; } /// - /// How many baets per beatlength to trigger. Defaults to 1. + /// How many beats per beatlength to trigger. Defaults to 1. /// public int Divisor { get; set; } = 1; From 1f3e1b308590409c6c09c33a91d62c88f3664962 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 17:59:07 +0900 Subject: [PATCH 167/189] Remove unused using --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8d16278f60..433becd8cc 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; From 5664ce3109d9cb40ede788e76b05cb5b503a237a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 18:51:12 +0900 Subject: [PATCH 168/189] Add hitobject container regression test --- .../Gameplay/TestSceneHitObjectContainer.cs | 75 +++++++++++++++++++ osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 + 2 files changed, 77 insertions(+) create mode 100644 osu.Game.Tests/Gameplay/TestSceneHitObjectContainer.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectContainer.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectContainer.cs new file mode 100644 index 0000000000..f2bfccb6de --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectContainer.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Gameplay +{ + [HeadlessTest] + public class TestSceneHitObjectContainer : OsuTestScene + { + private HitObjectContainer container; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = container = new HitObjectContainer(); + }); + + [Test] + public void TestLateHitObjectIsAddedEarlierInList() + { + DrawableHitObject hitObject = null; + + AddStep("setup", () => container.Add(new TestDrawableHitObject(new HitObject { StartTime = 500 }))); + + AddStep("add late hitobject", () => container.Add(hitObject = new TestDrawableHitObject(new HitObject { StartTime = 1000 }))); + + AddAssert("hitobject index is 0", () => container.IndexOf(hitObject) == 0); + } + + [Test] + public void TestEarlyHitObjectIsAddedLaterInList() + { + DrawableHitObject hitObject = null; + + AddStep("setup", () => container.Add(new TestDrawableHitObject(new HitObject { StartTime = 500 }))); + + AddStep("add early hitobject", () => container.Add(hitObject = new TestDrawableHitObject(new HitObject()))); + + AddAssert("hitobject index is 0", () => container.IndexOf(hitObject) == 1); + } + + [Test] + public void TestHitObjectsResortedAfterStartTimeChange() + { + DrawableHitObject firstObject = null; + DrawableHitObject secondObject = null; + + AddStep("setup", () => + { + container.Add(firstObject = new TestDrawableHitObject(new HitObject())); + container.Add(secondObject = new TestDrawableHitObject(new HitObject { StartTime = 1000 })); + }); + + AddStep("move first object after second", () => firstObject.HitObject.StartTime = 2000); + + AddAssert("first object index is 1", () => container.IndexOf(firstObject) == 0); + AddAssert("second object index is 0", () => container.IndexOf(secondObject) == 1); + } + + private class TestDrawableHitObject : DrawableHitObject + { + public TestDrawableHitObject([NotNull] HitObject hitObject) + : base(hitObject) + { + } + } + } +} diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1e33b15184..dea981c3ad 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.UI return true; } + public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); + private void onStartTimeChanged(DrawableHitObject hitObject) { if (!RemoveInternal(hitObject)) From cb5a35f8f9f23c5b53d456611d3cf6c65f16f86f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 21:08:38 +0900 Subject: [PATCH 169/189] Fix incorrect bindable valuechanged usage --- osu.Game/Screens/Select/FooterButtonMods.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index fbeda8f18e..dde6e90ee3 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -60,8 +60,6 @@ namespace osu.Game.Screens.Select AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 70 } }); - - Current.ValueChanged += _ => updateMultiplierText(); } [BackgroundDependencyLoader] @@ -75,6 +73,13 @@ namespace osu.Game.Screens.Select Hotkey = Key.F1; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateMultiplierText(), true); + } + private void updateMultiplierText() { var multiplier = Current.Value.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); From 2905a4418d5960f7edadaefedfb81545d4b7b943 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 21:30:57 +0900 Subject: [PATCH 170/189] Fix potential nullref --- osu.Game/Screens/Select/FooterButtonMods.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index dde6e90ee3..8419ee0c2a 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Select private void updateMultiplierText() { - var multiplier = Current.Value.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); + double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1; MultiplierText.Text = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; From 9ac15ef3a8141d0e583009aa7a604e112f6c1d06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 22:56:00 +0900 Subject: [PATCH 171/189] Don't log discord connection failures --- osu.Desktop/DiscordRichPresence.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index b53ca6161b..8818cef8eb 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -43,8 +43,7 @@ namespace osu.Desktop }; client.OnReady += onReady; - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); - client.OnConnectionFailed += (_, e) => Logger.Log($"An connection occurred with Discord RPC Client: {e.Type}", LoggingTarget.Network, LogLevel.Error); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u => { From 8d6987c8703d27b4459fb3f30f586a1b1b9ae8a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Dec 2019 23:59:48 +0900 Subject: [PATCH 172/189] Refactor storyboard timeline to reduce GC / fix crashes --- osu.Game/Storyboards/CommandTimeline.cs | 29 +++++---- osu.Game/Storyboards/CommandTimelineGroup.cs | 65 +++++++++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs index bcf642b4ea..c71806352d 100644 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ b/osu.Game/Storyboards/CommandTimeline.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 osu.Framework.Caching; using osu.Framework.Graphics; using System; using System.Collections.Generic; @@ -12,27 +11,35 @@ namespace osu.Game.Storyboards public class CommandTimeline : ICommandTimeline { private readonly List commands = new List(); + public IEnumerable Commands => commands.OrderBy(c => c.StartTime); + public bool HasCommands => commands.Count > 0; - private readonly Cached startTimeBacking = new Cached(); - public double StartTime => startTimeBacking.IsValid ? startTimeBacking : startTimeBacking.Value = HasCommands ? commands.Min(c => c.StartTime) : double.MinValue; + public double StartTime { get; private set; } = double.MaxValue; + public double EndTime { get; private set; } = double.MinValue; - private readonly Cached endTimeBacking = new Cached(); - public double EndTime => endTimeBacking.IsValid ? endTimeBacking : endTimeBacking.Value = HasCommands ? commands.Max(c => c.EndTime) : double.MaxValue; - - public T StartValue => HasCommands ? commands.OrderBy(c => c.StartTime).First().StartValue : default; - public T EndValue => HasCommands ? commands.OrderByDescending(c => c.EndTime).First().EndValue : default; + public T StartValue { get; private set; } + public T EndValue { get; private set; } public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue) { if (endTime < startTime) return; - commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, }); + commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue }); - startTimeBacking.Invalidate(); - endTimeBacking.Invalidate(); + if (startTime < StartTime) + { + StartValue = startValue; + StartTime = startTime; + } + + if (endTime > EndTime) + { + EndValue = endValue; + EndTime = endTime; + } } public override string ToString() diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 7b6e667d4f..6ce3b617e9 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.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; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics; @@ -25,28 +26,52 @@ namespace osu.Game.Storyboards public CommandTimeline FlipH = new CommandTimeline(); public CommandTimeline FlipV = new CommandTimeline(); + private readonly ICommandTimeline[] timelines; + + public CommandTimelineGroup() + { + timelines = new ICommandTimeline[] + { + X, + Y, + Scale, + VectorScale, + Rotation, + Colour, + Alpha, + BlendingParameters, + FlipH, + FlipV + }; + } + [JsonIgnore] - public IEnumerable Timelines + public double CommandsStartTime { get { - yield return X; - yield return Y; - yield return Scale; - yield return Rotation; - yield return Colour; - yield return Alpha; - yield return BlendingParameters; - yield return FlipH; - yield return FlipV; + double min = double.MaxValue; + + for (int i = 0; i < timelines.Length; i++) + min = Math.Min(min, timelines[i].StartTime); + + return min; } } [JsonIgnore] - public double CommandsStartTime => Timelines.Where(t => t.HasCommands).Min(t => t.StartTime); + public double CommandsEndTime + { + get + { + double max = double.MinValue; - [JsonIgnore] - public double CommandsEndTime => Timelines.Where(t => t.HasCommands).Max(t => t.EndTime); + for (int i = 0; i < timelines.Length; i++) + max = Math.Max(max, timelines[i].EndTime); + + return max; + } + } [JsonIgnore] public double CommandsDuration => CommandsEndTime - CommandsStartTime; @@ -61,7 +86,19 @@ namespace osu.Game.Storyboards public double Duration => EndTime - StartTime; [JsonIgnore] - public bool HasCommands => Timelines.Any(t => t.HasCommands); + public bool HasCommands + { + get + { + for (int i = 0; i < timelines.Length; i++) + { + if (timelines[i].HasCommands) + return true; + } + + return false; + } + } public virtual IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { From f38853d2291835e6cbda7cc12a2dcf1e4f9eae5f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Dec 2019 00:52:50 +0900 Subject: [PATCH 173/189] Improve performance of storyboard loading --- osu.Game/Storyboards/StoryboardSprite.cs | 83 ++++++++++++++++++------ 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index abf9f58804..22e1929419 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -7,6 +7,7 @@ using osu.Game.Storyboards.Drawables; using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; namespace osu.Game.Storyboards { @@ -63,50 +64,56 @@ namespace osu.Game.Storyboards public void ApplyTransforms(Drawable drawable, IEnumerable> triggeredGroups = null) { - applyCommands(drawable, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), + // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. + // To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list + // The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially. + + List generated = new List(); + + generateCommands(generated, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing)); + generateCommands(generated, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing)); + generateCommands(generated, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); + generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); + generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); + generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); + generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), false); if (drawable is IVectorScalable vectorScalable) { - applyCommands(drawable, getCommands(g => g.VectorScale, triggeredGroups), (d, value) => vectorScalable.VectorScale = value, + generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (d, value) => vectorScalable.VectorScale = value, (d, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing)); } if (drawable is IFlippable flippable) { - applyCommands(drawable, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), + generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), false); - applyCommands(drawable, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), + generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), false); } + + foreach (var command in generated.OrderBy(g => g.StartTime)) + command.ApplyTo(drawable); } - private void applyCommands(Drawable drawable, IEnumerable.TypedCommand> commands, DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, - bool alwaysInitialize = true) - where T : struct + private void generateCommands(List resultList, IEnumerable.TypedCommand> commands, + DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, bool alwaysInitialize = true) { - var initialized = false; + bool initialized = false; - foreach (var command in commands.OrderBy(l => l)) + foreach (var command in commands) { + DrawablePropertyInitializer initFunc = null; + if (!initialized) { if (alwaysInitialize || command.StartTime == command.EndTime) - initializeProperty.Invoke(drawable, command.StartValue); + initFunc = initializeProperty; initialized = true; } - using (drawable.BeginAbsoluteSequence(command.StartTime)) - { - transform(drawable, command.StartValue, 0, Easing.None); - transform(drawable, command.EndValue, command.Duration, command.Easing); - } + resultList.Add(new GeneratedCommand(command, initFunc, transform)); } } @@ -127,5 +134,39 @@ namespace osu.Game.Storyboards public override string ToString() => $"{Path}, {Origin}, {InitialPosition}"; + + private interface IGeneratedCommand + { + double StartTime { get; } + + void ApplyTo(Drawable drawable); + } + + private readonly struct GeneratedCommand : IGeneratedCommand + { + public double StartTime => command.StartTime; + + private readonly DrawablePropertyInitializer initializeProperty; + private readonly DrawableTransformer transform; + private readonly CommandTimeline.TypedCommand command; + + public GeneratedCommand([NotNull] CommandTimeline.TypedCommand command, [CanBeNull] DrawablePropertyInitializer initializeProperty, [NotNull] DrawableTransformer transform) + { + this.command = command; + this.initializeProperty = initializeProperty; + this.transform = transform; + } + + public void ApplyTo(Drawable drawable) + { + initializeProperty?.Invoke(drawable, command.StartValue); + + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + transform(drawable, command.StartValue, 0, Easing.None); + transform(drawable, command.EndValue, command.Duration, command.Easing); + } + } + } } } From 007e2e80c1cd193e9e7a1ded3df6191571bed8a3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Dec 2019 02:02:57 +0900 Subject: [PATCH 174/189] Refactor to fix sorting issues --- osu.Game/Overlays/SocialOverlay.cs | 70 +++++++++++++----------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 5b3acf8e46..01dd1ee635 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.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; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -15,7 +16,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.Social; using osu.Game.Users; -using System; using System.Threading; using osu.Framework.Threading; @@ -33,17 +33,18 @@ namespace osu.Game.Overlays protected override SearchableListHeader CreateHeader() => new Header(); protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); - private IEnumerable users; + private User[] users = Array.Empty(); - public IEnumerable Users + public User[] Users { get => users; set { - if (ReferenceEquals(users, value)) + if (users == value) return; - users = value?.ToList(); + users = value ?? Array.Empty(); + recreatePanels(); } } @@ -72,8 +73,8 @@ namespace osu.Game.Overlays Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate(); - Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); - Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => updateUsers(Users); + Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels(); + Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => recreatePanels(); currentQuery.BindTo(Filter.Search.Current); currentQuery.ValueChanged += query => @@ -115,19 +116,19 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += updateUsers; + friendRequest.Success += users => Users = users.ToArray(); API.Queue(getUsersRequest = friendRequest); break; default: var userRequest = new GetUsersRequest(); // TODO filter arguments! - userRequest.Success += res => updateUsers(res.Users.Select(r => r.User)); + userRequest.Success += res => Users = res.Users.Select(r => r.User).ToArray(); API.Queue(getUsersRequest = userRequest); break; } } - private void recreatePanels(PanelDisplayStyle displayStyle) + private void recreatePanels() { clearPanels(); @@ -139,17 +140,33 @@ namespace osu.Game.Overlays loadCancellation = new CancellationTokenSource(); + IEnumerable sortedUsers = Users; + + switch (Filter.Tabs.Current.Value) + { + case SocialSortCriteria.Location: + sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName); + break; + + case SocialSortCriteria.Name: + sortedUsers = sortedUsers.OrderBy(u => u.Username); + break; + } + + if (Filter.DisplayStyleControl.Dropdown.Current.Value == SortDirection.Descending) + sortedUsers = sortedUsers.Reverse(); + var newPanels = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(10f), Margin = new MarginPadding { Top = 10 }, - ChildrenEnumerable = Users.Select(u => + ChildrenEnumerable = sortedUsers.Select(u => { SocialPanel panel; - switch (displayStyle) + switch (Filter.DisplayStyleControl.DisplayStyle.Value) { case PanelDisplayStyle.Grid: panel = new SocialGridPanel(u) @@ -188,36 +205,9 @@ namespace osu.Game.Overlays return; } - updateUsers(Users); + recreatePanels(); } - private void updateUsers(IEnumerable newUsers) - { - var sortDirection = Filter.DisplayStyleControl.Dropdown.Current.Value; - - IEnumerable sortedUsers = newUsers; - - if (sortedUsers.Any()) - { - switch (Filter.Tabs.Current.Value) - { - case SocialSortCriteria.Location: - sortedUsers = sortBy(sortedUsers, u => u.Country.FullName, sortDirection); - break; - - case SocialSortCriteria.Name: - sortedUsers = sortBy(sortedUsers, u => u.Username, sortDirection); - break; - } - } - - Users = sortedUsers; - recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); - } - - private IEnumerable sortBy(IEnumerable users, Func condition, SortDirection sortDirection) => - sortDirection == SortDirection.Ascending ? users.OrderBy(condition) : users.OrderByDescending(condition); - private void clearPanels() { loading.Show(); From dd68106d909d9deffb321a39536fbb35454bf05d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Dec 2019 02:21:33 +0900 Subject: [PATCH 175/189] Recreate panels only while loading/loaded --- osu.Game/Overlays/SocialOverlay.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 01dd1ee635..7cc3b6e3ce 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.SearchableList; using osu.Game.Overlays.Social; using osu.Game.Users; using System.Threading; +using osu.Framework.Allocation; using osu.Framework.Threading; namespace osu.Game.Overlays @@ -44,7 +45,9 @@ namespace osu.Game.Overlays return; users = value ?? Array.Empty(); - recreatePanels(); + + if (LoadState >= LoadState.Ready) + recreatePanels(); } } @@ -70,7 +73,6 @@ namespace osu.Game.Overlays }; Header.Tabs.Current.ValueChanged += _ => queueUpdate(); - Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate(); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels(); @@ -88,6 +90,12 @@ namespace osu.Game.Overlays }; } + [BackgroundDependencyLoader] + private void load() + { + recreatePanels(); + } + private APIRequest getUsersRequest; private readonly Bindable currentQuery = new Bindable(); From a46602f705c2686ad45a58ea2997bb715bb8265e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2019 11:26:22 +0900 Subject: [PATCH 176/189] Move cancellation token construction closer to usage --- osu.Game/Overlays/SocialOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 7cc3b6e3ce..0c99962def 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -146,8 +146,6 @@ namespace osu.Game.Overlays return; } - loadCancellation = new CancellationTokenSource(); - IEnumerable sortedUsers = Users; switch (Filter.Tabs.Current.Value) @@ -202,7 +200,7 @@ namespace osu.Game.Overlays loading.Hide(); ScrollFlow.Add(panels = newPanels); - }, loadCancellation.Token); + }, (loadCancellation = new CancellationTokenSource()).Token); } private void onFilterUpdate() From b1533ae2a9d32eee264464d1d12ee374c59cd1bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2019 14:58:56 +0900 Subject: [PATCH 177/189] Fix score serialisation failing for unknown mod properties --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c7609e8a0b..c37bab9086 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -89,7 +89,7 @@ namespace osu.Game.Scoring if (mods == null) return null; - return modsJson = JsonConvert.SerializeObject(mods); + return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym })); } set { From 81e842f6b4bab33100c8f79eae63c6ab41155730 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2019 19:48:48 +0900 Subject: [PATCH 178/189] Fix waveform test beatmap accessing zip archive across multiple threads --- .../Editor/TestSceneEditorComposeTimeline.cs | 14 +++----- osu.Game.Tests/WaveformTestBeatmap.cs | 32 +++++++++++-------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs index e618256c03..ed6bc5fe0c 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs @@ -68,8 +68,11 @@ namespace osu.Game.Tests.Visual.Editor { private readonly Drawable marker; - private readonly IBindable beatmap = new Bindable(); - private IAdjustableClock adjustableClock; + [Resolved] + private IBindable beatmap { get; set; } + + [Resolved] + private IAdjustableClock adjustableClock { get; set; } public AudioVisualiser() { @@ -91,13 +94,6 @@ namespace osu.Game.Tests.Visual.Editor }; } - [BackgroundDependencyLoader] - private void load(IAdjustableClock adjustableClock, IBindable beatmap) - { - this.adjustableClock = adjustableClock; - this.beatmap.BindTo(beatmap); - } - protected override void Update() { base.Update(); diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 0d16a78f75..59f322e24c 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -20,23 +20,21 @@ namespace osu.Game.Tests /// public class WaveformTestBeatmap : WorkingBeatmap { - private readonly ZipArchiveReader reader; - private readonly Stream stream; private readonly ITrackStore trackStore; + private Stream getStream() => TestResources.GetTestBeatmapStream(); + + private ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream()); + public WaveformTestBeatmap(AudioManager audioManager) : base(new BeatmapInfo(), audioManager) { - stream = TestResources.GetTestBeatmapStream(); - reader = new ZipArchiveReader(stream); - trackStore = audioManager.GetTrackStore(reader); + trackStore = audioManager.GetTrackStore(getZipReader()); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - stream?.Dispose(); - reader?.Dispose(); trackStore?.Dispose(); } @@ -50,15 +48,23 @@ namespace osu.Game.Tests protected override Track GetTrack() => trackStore.Get(firstAudioFile); - private string firstAudioFile => reader.Filenames.First(f => f.EndsWith(".mp3")); - - private Stream getBeatmapStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))); + private string firstAudioFile + { + get + { + using (var reader = getZipReader()) + return reader.Filenames.First(f => f.EndsWith(".mp3")); + } + } private Beatmap createTestBeatmap() { - using (var beatmapStream = getBeatmapStream()) - using (var beatmapReader = new LineBufferedReader(beatmapStream)) - return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); + using (var reader = getZipReader()) + { + using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")))) + using (var beatmapReader = new LineBufferedReader(beatmapStream)) + return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); + } } } } From a3154f2f7b3046db8930de15b55636adf6f510d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2019 20:30:58 +0900 Subject: [PATCH 179/189] 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 abb3cc8244..dd11804b90 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,6 +54,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e5f34b1c7e..757e0e11fa 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c84e617285..0dba92b975 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -74,7 +74,7 @@ - + @@ -82,7 +82,7 @@ - + From 3ac0e3ce51cd811cb2cc07afbfd329aae5e853a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2019 23:03:31 +0900 Subject: [PATCH 180/189] Fix iOS project failing to compile --- osu.Game.Rulesets.Catch.Tests.iOS/Application.cs | 2 +- osu.Game.Rulesets.Mania.Tests.iOS/Application.cs | 2 +- osu.Game.Rulesets.Osu.Tests.iOS/Application.cs | 2 +- osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs | 2 +- osu.Game.Tests.iOS/Application.cs | 2 +- osu.iOS/Application.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs index beca477943..f7f07ef938 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs @@ -5,7 +5,7 @@ using UIKit; namespace osu.Game.Rulesets.Catch.Tests.iOS { - public class Application + public static class Application { public static void Main(string[] args) { diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs index 0362402320..c381ea585d 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs @@ -5,7 +5,7 @@ using UIKit; namespace osu.Game.Rulesets.Mania.Tests.iOS { - public class Application + public static class Application { public static void Main(string[] args) { diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs index 3718264a42..b36d0b5728 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs @@ -5,7 +5,7 @@ using UIKit; namespace osu.Game.Rulesets.Osu.Tests.iOS { - public class Application + public static class Application { public static void Main(string[] args) { diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs index 330cb42901..73faf16d9f 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs @@ -5,7 +5,7 @@ using UIKit; namespace osu.Game.Rulesets.Taiko.Tests.iOS { - public class Application + public static class Application { public static void Main(string[] args) { diff --git a/osu.Game.Tests.iOS/Application.cs b/osu.Game.Tests.iOS/Application.cs index d96a3e27a4..9533b90131 100644 --- a/osu.Game.Tests.iOS/Application.cs +++ b/osu.Game.Tests.iOS/Application.cs @@ -5,7 +5,7 @@ using UIKit; namespace osu.Game.Tests.iOS { - public class Application + public static class Application { public static void Main(string[] args) { diff --git a/osu.iOS/Application.cs b/osu.iOS/Application.cs index 30e0e15ad1..740937e0e1 100644 --- a/osu.iOS/Application.cs +++ b/osu.iOS/Application.cs @@ -5,7 +5,7 @@ using UIKit; namespace osu.iOS { - public class Application + public static class Application { public static void Main(string[] args) { From 7d090d6cd90fd14810aa84d0c30d549f198c09a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 19 Dec 2019 17:52:58 +0300 Subject: [PATCH 181/189] Fix key overlay appearing regardless of the setting --- osu.Game/Screens/Play/KeyCounterDisplay.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index 1edb95ca46..9c107f0293 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -43,6 +43,11 @@ namespace osu.Game.Screens.Play private void load(OsuConfigManager config) { config.BindWith(OsuSetting.KeyOverlay, configVisibility); + } + + protected override void LoadComplete() + { + base.LoadComplete(); Visible.BindValueChanged(_ => updateVisibility()); configVisibility.BindValueChanged(_ => updateVisibility(), true); From 656c58450305770ec5f76a7742cf3964e74c40aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2019 13:50:47 +0900 Subject: [PATCH 182/189] Update RestoreDefaultValueButton when default value changes --- osu.Game/Overlays/Settings/SettingsItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 9c390c34ec..31fcb7abd8 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -113,6 +113,7 @@ namespace osu.Game.Overlays.Settings bindable = value; bindable.ValueChanged += _ => UpdateState(); bindable.DisabledChanged += _ => UpdateState(); + bindable.DefaultChanged += _ => UpdateState(); UpdateState(); } } From de8154bc7f5af6a970715c35b347d7900915f34b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2019 13:54:13 +0900 Subject: [PATCH 183/189] Update readme with new testflight link source --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2e854c755..753dee548b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you are not interested in developing the game, you can still consume our [bin **Latest build:** -| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [iOS(iOS 10+)](https://testflight.apple.com/join/2tLcjWlF) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) +| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | ------------- | ------------- | ------------- | ------------- | - **Linux** users are recommended to self-compile until we have official deployment in place. From 11214628adb34e9cec915706c38b9efae6833d64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2019 13:50:57 +0900 Subject: [PATCH 184/189] Add post-update notification for iOS users --- osu.Game/Updater/UpdateManager.cs | 5 ++++- osu.iOS/OsuGameIOS.cs | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index e256cdbe45..48505a9891 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -11,7 +11,10 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Updater { - public abstract class UpdateManager : CompositeDrawable + /// + /// An update manager which only shows notifications after an update completes. + /// + public class UpdateManager : CompositeDrawable { [Resolved] private OsuConfigManager config { get; set; } diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 6cf18df9a6..e5ff4aec95 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -4,11 +4,19 @@ using System; using Foundation; using osu.Game; +using osu.Game.Updater; namespace osu.iOS { public class OsuGameIOS : OsuGame { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Add(new UpdateManager()); + } } } From 1802e0ff111559f07dd28eba251498d3e2876b1b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Dec 2019 16:04:05 +0900 Subject: [PATCH 185/189] Fix storyboard incorrectly re-ordering elements --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 67c4105e6d..b1b27278fe 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using osuTK; using osuTK.Graphics; using osu.Framework.Extensions; @@ -42,10 +41,6 @@ namespace osu.Game.Beatmaps.Formats { this.storyboard = storyboard; base.ParseStreamInto(stream, storyboard); - - // OrderBy is used to guarantee that the parsing order of elements with equal start times is maintained (stably-sorted) - foreach (StoryboardLayer layer in storyboard.Layers) - layer.Elements = layer.Elements.OrderBy(h => h.StartTime).ToList(); } protected override void ParseLine(Storyboard storyboard, Section section, string line) From 705cdde148db14e740e51d02662a0152800b5666 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Dec 2019 16:42:45 +0900 Subject: [PATCH 186/189] Fix incorrect test --- osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 66d53d7e7b..96ff6b81e3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.IsTrue(sprite.IsDrawable); Assert.AreEqual(Anchor.Centre, sprite.Origin); - Assert.AreEqual("SB/black.jpg", sprite.Path); + Assert.AreEqual("SB/lyric/ja-21.png", sprite.Path); var animation = background.Elements.OfType().First(); Assert.NotNull(animation); From 351e826120093ea0fb383f37237b361d4debd741 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 16 Dec 2019 15:28:18 +0800 Subject: [PATCH 187/189] Upgrade project targets to netcoreapp3.1 --- .../CatchRuleset__Tests_.xml | 4 +-- .../ManiaRuleset__Tests_.xml | 4 +-- .../runConfigurations/OsuRuleset__Tests_.xml | 4 +-- .../TaikoRuleset__Tests_.xml | 4 +-- .../.idea/runConfigurations/Tournament.xml | 4 +-- .../runConfigurations/Tournament__Tests_.xml | 4 +-- .../.idea/runConfigurations/osu_.xml | 4 +-- .../.idea/runConfigurations/osu___Tests_.xml | 4 +-- .vscode/launch.json | 32 +++++++++---------- .vscode/tasks.json | 2 +- README.md | 4 +-- osu.Desktop/osu.Desktop.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- 18 files changed, 42 insertions(+), 42 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml index 5372b6f28a..a4154623b6 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml @@ -1,6 +1,6 @@ - WinExe - netcoreapp3.0 + netcoreapp3.1 diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 8fc4dbfe72..dea6e6c0fb 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -9,7 +9,7 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index fddf176fd0..9d4e016eae 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -9,7 +9,7 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index b5bd384e05..d728d65bfd 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -9,7 +9,7 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c5998c9cfc..6c799e5e90 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -10,7 +10,7 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index d58a724c27..7ecfd6ef70 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -11,7 +11,7 @@ WinExe - netcoreapp3.0 + netcoreapp3.1 From 0ebdf90dfa9d4ed99b6df992844fe16a85beaf33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Dec 2019 18:07:39 +0900 Subject: [PATCH 188/189] Move methods below ctor --- osu.Game.Tests/WaveformTestBeatmap.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 59f322e24c..b7d7bb1ee1 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -22,10 +22,6 @@ namespace osu.Game.Tests { private readonly ITrackStore trackStore; - private Stream getStream() => TestResources.GetTestBeatmapStream(); - - private ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream()); - public WaveformTestBeatmap(AudioManager audioManager) : base(new BeatmapInfo(), audioManager) { @@ -38,6 +34,10 @@ namespace osu.Game.Tests trackStore?.Dispose(); } + private Stream getStream() => TestResources.GetTestBeatmapStream(); + + private ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream()); + protected override IBeatmap GetBeatmap() => createTestBeatmap(); protected override Texture GetBackground() => null; From 492a91067191708b5e4d1896181249a4c0fcb3ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2019 19:08:05 +0900 Subject: [PATCH 189/189] Update missed launch configurations --- osu.Game.Rulesets.Catch.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Mania.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Osu.Tests/.vscode/launch.json | 4 ++-- osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json index 4030d2d9e7..67d27c33eb 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp3.0/osu.Game.Rulesets.Catch.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/netcoreapp3.0/osu.Game.Rulesets.Catch.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json index 779eb4f277..0811c2724c 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp3.0/osu.Game.Rulesets.Mania.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/netcoreapp3.0/osu.Game.Rulesets.Mania.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json index 67338b7bbe..94568e3852 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp3.0/osu.Game.Rulesets.Osu.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/netcoreapp3.0/osu.Game.Rulesets.Osu.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json index 7d929e6bbf..5b02ecfc91 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp3.0/osu.Game.Rulesets.Taiko.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp3.1/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", @@ -20,7 +20,7 @@ "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Release/netcoreapp3.0/osu.Game.Rulesets.Taiko.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp3.1/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)",