From 65d664425bdfeca60343863d1ef441a7ade7a389 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 12 Apr 2019 23:13:13 +0200 Subject: [PATCH 01/96] 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 02/96] 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 03/96] 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 04/96] 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 05/96] 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 06/96] 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 07/96] 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 08/96] 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 09/96] 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 10/96] 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 11/96] 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 12/96] 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 13/96] 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 20edaf4ba6a04adf9836ef4c3f2b2b61991e6a21 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sat, 23 Nov 2019 17:32:16 +0000 Subject: [PATCH 14/96] 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 15/96] 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 16/96] 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 17/96] 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 18/96] 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 19/96] 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 88ec0c14862a862750060e0717324cf2c03606f9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Dec 2019 18:49:58 +0100 Subject: [PATCH 20/96] 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 6c1ae3bc8ac77bd1babbd0f0395071fd583a905a Mon Sep 17 00:00:00 2001 From: Albie Date: Tue, 10 Dec 2019 16:59:31 +0000 Subject: [PATCH 21/96] 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 22/96] 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 23/96] 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 24/96] 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 25/96] 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 26/96] 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 27/96] 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 28/96] 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 ad2528d4d2e03a1c355c84934dd73104b1afb81e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 15:05:29 +0900 Subject: [PATCH 29/96] Hide key counter along with other hud elements Also tidies up HUD hide logic and protects against incorrect hiding. --- osu.Game/Screens/Play/HUDOverlay.cs | 60 ++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 0f9edf5606..35157fca58 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -43,8 +44,15 @@ namespace osu.Game.Screens.Play private readonly DrawableRuleset drawableRuleset; private readonly IReadOnlyList mods; - private Bindable showHud; + /// + /// Whether the elements that can optionally be hidden should be visible. + /// + public Bindable ShowHud { get; } = new BindableBool(); + + private Bindable configShowHud; + private readonly Container visibilityContainer; + private readonly BindableBool replayLoaded = new BindableBool(); private static bool hasShownNotificationOnce; @@ -53,6 +61,8 @@ namespace osu.Game.Screens.Play private readonly Container topScoreContainer; + private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; + public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) { this.scoreProcessor = scoreProcessor; @@ -73,8 +83,6 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - AutoSizeDuration = 200, - AutoSizeEasing = Easing.Out, Children = new Drawable[] { AccuracyCounter = CreateAccuracyCounter(), @@ -95,6 +103,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), AutoSizeAxes = Axes.Both, + AutoSizeDuration = 150, + AutoSizeEasing = Easing.OutQuint, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -118,8 +128,29 @@ namespace osu.Game.Screens.Play ModDisplay.Current.Value = mods; - showHud = config.GetBindable(OsuSetting.ShowInterface); - showHud.BindValueChanged(visible => visibilityContainer.FadeTo(visible.NewValue ? 1 : 0, duration, easing), true); + configShowHud = config.GetBindable(OsuSetting.ShowInterface); + + if (!configShowHud.Value && !hasShownNotificationOnce) + { + hasShownNotificationOnce = true; + + notificationOverlay?.Post(new SimpleNotification + { + Text = @"The score overlay is currently disabled. You can toggle this by pressing Shift+Tab." + }); + } + + // start all elements hidden + hideTargets.ForEach(d => d.Hide()); + } + + public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, duration, easing))); ShowHealthbar.BindValueChanged(healthBar => { @@ -135,20 +166,11 @@ namespace osu.Game.Screens.Play } }, true); - if (!showHud.Value && !hasShownNotificationOnce) + configShowHud.BindValueChanged(visible => { - hasShownNotificationOnce = true; - - notificationOverlay?.Post(new SimpleNotification - { - Text = @"The score overlay is currently disabled. You can toggle this by pressing Shift+Tab." - }); - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); + if (!ShowHud.Disabled) + ShowHud.Value = visible.NewValue; + }, true); replayLoaded.BindValueChanged(replayLoadedValueChanged, true); } @@ -189,7 +211,7 @@ namespace osu.Game.Screens.Play switch (e.Key) { case Key.Tab: - showHud.Value = !showHud.Value; + configShowHud.Value = !configShowHud.Value; return true; } } From ffb5cdc6aef7056a8430cd76f79299d5ee087d84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 14:19:04 +0900 Subject: [PATCH 30/96] Hide settings overlay along with other HUD-hidden content --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 35157fca58..64c3cddf2a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -94,9 +94,9 @@ namespace osu.Game.Screens.Play Progress = CreateProgress(), ModDisplay = CreateModsContainer(), HitErrorDisplay = CreateHitErrorDisplayOverlay(), + PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), } }, - PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), new FillFlowContainer { Anchor = Anchor.BottomRight, From 3ccfee64f6ff44c82f3b66270f790b951c745da5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 16:09:42 +0900 Subject: [PATCH 31/96] Add HUDOverlay tests --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 81 +++++++++++++++++++ osu.Game/Screens/Play/HUD/HitErrorDisplay.cs | 7 +- osu.Game/Screens/Play/HUDOverlay.cs | 19 +++-- 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs new file mode 100644 index 0000000000..39c42980ab --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -0,0 +1,81 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneHUDOverlay : ManualInputManagerTestScene + { + private HUDOverlay hudOverlay; + + private Drawable hideTarget => hudOverlay.KeyCounter; // best way of checking hideTargets without exposing. + + [Resolved] + private OsuConfigManager config { get; set; } + + [Test] + public void TestShownByDefault() + { + createNew(); + + AddAssert("showhud is set", () => hudOverlay.ShowHud.Value); + + AddAssert("hidetarget is visible", () => hideTarget.IsPresent); + AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent); + } + + [Test] + public void TestFadesInOnLoadComplete() + { + float? initialAlpha = null; + + createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); + AddUntilStep("wait for load", () => hudOverlay.IsAlive); + AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1); + } + + [Test] + public void TestHideExternally() + { + createNew(); + + AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); + + AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); + } + + [Test] + public void TestExternalHideDoesntAffectConfig() + { + bool originalConfigValue = false; + + createNew(); + + AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.ShowInterface)); + + AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); + AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface)); + + AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); + AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface)); + } + + private void createNew(Action action = null) + { + AddStep("create overlay", () => + { + Child = hudOverlay = new HUDOverlay(null, null, Array.Empty()); + + action?.Invoke(hudOverlay); + }); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs index 54556f8648..6196ce4026 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs @@ -31,7 +31,8 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.Both; - processor.NewJudgement += onNewJudgement; + if (processor != null) + processor.NewJudgement += onNewJudgement; } [BackgroundDependencyLoader] @@ -96,7 +97,9 @@ namespace osu.Game.Screens.Play.HUD protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - processor.NewJudgement -= onNewJudgement; + + if (processor != null) + processor.NewJudgement -= onNewJudgement; } } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 64c3cddf2a..7df780b678 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -118,13 +118,18 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader(true)] private void load(OsuConfigManager config, NotificationOverlay notificationOverlay) { - BindProcessor(scoreProcessor); - BindDrawableRuleset(drawableRuleset); + if (scoreProcessor != null) + BindProcessor(scoreProcessor); - Progress.Objects = drawableRuleset.Objects; - Progress.AllowSeeking = drawableRuleset.HasReplayLoaded.Value; - Progress.RequestSeek = time => RequestSeek(time); - Progress.ReferenceClock = drawableRuleset.FrameStableClock; + if (drawableRuleset != null) + { + BindDrawableRuleset(drawableRuleset); + + Progress.Objects = drawableRuleset.Objects; + Progress.AllowSeeking = drawableRuleset.HasReplayLoaded.Value; + Progress.RequestSeek = time => RequestSeek(time); + Progress.ReferenceClock = drawableRuleset.FrameStableClock; + } ModDisplay.Current.Value = mods; @@ -279,7 +284,7 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20, Right = 10 }, }; - protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset.FirstAvailableHitWindows); + protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset?.FirstAvailableHitWindows); protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); From 94f3dbb2f65afef1aaff9f63a7070efa11f0985b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 16:09:50 +0900 Subject: [PATCH 32/96] Adjust transitions slightly --- osu.Game/Screens/Play/HUDOverlay.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7df780b678..dc32fc7cd5 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -24,8 +24,8 @@ namespace osu.Game.Screens.Play { public class HUDOverlay : Container { - private const int duration = 250; - private const Easing easing = Easing.OutQuint; + private const int fade_duration = 400; + private const Easing fade_easing = Easing.Out; public readonly KeyCounterDisplay KeyCounter; public readonly RollingCounter ComboCounter; @@ -103,8 +103,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), AutoSizeAxes = Axes.Both, - AutoSizeDuration = 150, - AutoSizeEasing = Easing.OutQuint, + AutoSizeDuration = fade_duration, + AutoSizeEasing = fade_easing, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -155,19 +155,19 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, duration, easing))); + ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, fade_duration, fade_easing))); ShowHealthbar.BindValueChanged(healthBar => { if (healthBar.NewValue) { - HealthDisplay.FadeIn(duration, easing); - topScoreContainer.MoveToY(30, duration, easing); + HealthDisplay.FadeIn(fade_duration, fade_easing); + topScoreContainer.MoveToY(30, fade_duration, fade_easing); } else { - HealthDisplay.FadeOut(duration, easing); - topScoreContainer.MoveToY(0, duration, easing); + HealthDisplay.FadeOut(fade_duration, fade_easing); + topScoreContainer.MoveToY(0, fade_duration, fade_easing); } }, true); From e4297ffeaded24ea61f8fd188e7fb92215d70674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2019 15:09:55 +0900 Subject: [PATCH 33/96] 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 34/96] 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 35/96] 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 c6cbf0f28a968ce42c5ba3f7731f98da822ac7e6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 12 Dec 2019 16:27:11 +0300 Subject: [PATCH 36/96] 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 z6cQGHfEVBa;GUuvimOb27aBHzAOtWD&>t`g;BCSC?-5c|0tx{cfCBQjtcaqNfd_B_ z%%bojsk4i~@2yuVm6zeq3xFR0DVzX~7i`gyl+pY4lmc5@TkBLRRWcz&oYXvoln8*a z`gMZowzy$aQ`3E;(O9EatB1O|xp~{-p3Tk8$#AcK!=J|hdQO1uO9WsAARM5!`fPjf4*+WBl0tNv3kuaeqi7(~|N=v>Ah>H9_zE8b-_wJ`kN=jbU>-C>QYtOah z`vD=d;1X#kU3Cl2MgTQnCE#{P3We6yQdCs*&4mjWK3ceN;WYRfV*ta6!O_@#h{2S0 zR-Q61FV6?a@Gf|mW1SHJyix&N7IOj!16zD?M+&T|smax7G@q|pwQ3IO>OBP*=qQ7C zhPt}CXMqSGwzRZlT0n#dPJnhy0Q}fNYd1hEI}jTiyOi|neE@@rOYd=`ivhyJ0>I9W zh)`W!eHJdGdMg3?1O5(Z5>gIi=M6bIIs2ocqA&@3nhfBedv|kIql;h~c!X_YBmj~$ zR#jDPCV5Y-6;04$rq!;K~mfgB_ zYnB`cuvX{?LgJIXfB*j9lgxf7>B^~kiQ$F1z_$ewM8l06H~t`H0thBOMMXs?;jNQM zs~;eP1kV=0UQxWzpjUc&`bSbD0LJfFA>(T_nqBa=@njIEH@A8*2!;gc68}>w1i<0k z1UMwnrMbRwYx$DD?ZxhU52h(tzATBQM)gB`N+#m$dz&z`Iuy5bKW$^MyZhX5WhGtSxPxz-Rz6X1!y{^iS;x4=uI$-^9;@YxUB zS>0c{bm>%Ay4NmUG(OJKeUGlwYPIpf!NJk+X9WPmLaTy{z2qzzO?}Q(;pgWU;O*^Q zJ8jxD`z}-=k9ExkC>}n1c!5019n1%L910CH#jNVBx84f2<{qjpaNu8LK*u!yy(uOp zW;<#5dYsq58>qwNGrBNn(4fI9R;-9oDwTb#SyyLG>&NW=UApedl`GrceDh6=KiEZS z<~DJtWRb-^v#C%p+TGo~Dl9B4#n!GLv;GR&<>+*}L^5x$AJ6!E6lSxk?}PaG_>8u7 z2>5`k&odFC`QX8Wd$8r>YOFSiZxkSL*+v(@_&z&!?D!Z$fznp3KY*RSotc@rlg!^! z^A_0ii#x~^dwM{we#(?7Gi)*rz|LaDwRmLaE_eOTLx552P;o*+!nd6@P?ZJ7p8%J| zbX|Ua{$cL=olC&m>);Yi*M*0N&k70(3hS=>Kg#qqv&dY&KD_lg*8)#6uh49G9`dw= zj=X@X6XW}{t-qk4;2>P;00!RfoX!j2h7|kgx|d&mdEUs8BPVyY^~W%=Ka)J= zF7YM{N=j+Fefi~=2gOEcRUH@~NBfr1b%lk638eM&0Uzh}0=NUpZn`cqGI9a^+<=yj zLckJw$XBn|-wX{6-NzF@NyvWyji$oTp+mjbty{O$S^})33ne8brxop+9`V-i{9XX} zGyDf#w|Md5*-$^qA_9=E-!yvEFF85+7;pWOk_&WU%9JTVzP`SeC4*q&59uVLxVZSg ziuT1Ee3FNRHhO~BuF-akj*dR0V3y&Dg#fdzUukJ+DqNcQBoE0+B3)RpU_mfk0y}H{ zYy(87%) ztE=0xWy|J6y=zBF;7fF&va%8<78rQ)Cyy{NbLLD80iy*8fXljB50H|Q@&oVsNm|Ud zsSODU37`qU#*E3#qJvyT`)XeP$X9|n0I{hnyQFOF*s-g{a)1}~3FK(s(_<#&0MF1TC&oz||21B8{yL%AM zFXZ}j`Z(WdB1re-Fc%y=c<`E6}Pmspa&~C4d(b0xG%AWF=*E!OP3*@!3kE9smeX%5^4R0)PiV0(j5_=+EQ;wOnVi60>z2 zuCA^wECI~K&CQL+e|d@7c#c}F#=V+dL_I)jYpb5?O!gkYRn!9jL%0W!znFOdvlj0G z!u2C7Q8P*Y<3TX+*O}}+fKesZPlR5;P_8psiP=D3 zO-)TB>j5g6UceBpGg--Sx=>zTj;p*IMLmF0sr2SLldlJ;s;X*$OFc~h9YcUoTxYTp zGka>a+8Ve#q6uL3yjZ1Djo~_zm6*-<$jZvXkY6K80QL=Bq$^}f*jTFEdiCnnYL)=F zsepkB0k|ExJ6E49B!DhdfavvloF-PQq707f|86pG4CK0#g_uqEFD@>Ahh`k)xWtrSe1?%&VP z?~?})9vp+$@;dYYBn(`qs&G2d60U0LX8+f(U(W&KH?WPkqlF>6m@1wJNJQ|IH@N(n zt~+?}AZ{3mJH8k!A^=ESYinx*RSp5H;Odox;Mo8_&jKzlFE5LUiNUNtoBc;s2XRH; z($aE(spbT5{??Ck|K6tSjvhUF5iXVRT9MVR1UD9Bj;deEH)nL}GlM>$ja38Ezkh!` z^)=Y)Q^WA&iARqfonorr=IzXBTR*z}WprIaLc&G8UXQbXa=LocNdjJA1#fvyd2ZnQ z%vQhp;lqc22ro_P^2wpN<#=ObVDnRINSi5%Z|KQJbw(Dm#0WcS6XlRIKsy|UMJBxDZ zvDW_*+xib2IFO{%>9F-*v(<-q84QM6CKvdM$6%+xdD zi{NA4Xu6`Ip`m`!qD6eI&{)10w5q6IV(VdWI!M{WS1}xaEn#wsZ*ztNyh(t zn;lObwVD8U^xVCB_cS6o!BlQgtMD2d^HEY#Qmzr>&$jomvt1#8B0fI8Z((8KRRI!| z0AAsSwFvJB=&vm)Dd`e1{!5NHceqOgKtx4FsdI92gu(&Jd)&YsVuz5@jvYIeM2tVr zQ3p?VnE>R*N{vQyT!aMtG4hk>3y2p|7D@Qf_%j@N@_4rhV8X?V7k7z};77o8ZurMo zDwOPDKYz=XEr)HD@bw4*5OHyF?>2#m5aEMfZ1_Q9E+40JzwyQ!Uyy$OvyPZjZZ`rT zqNAhdmX?-25Fvr47p^y0i3>G^l%dsXZwCbheG2M*2jJtVH3fDh00J28lbV{E+$Jtx z^n%GH81{||{dgSR!(@+4=XxE`&ykBt>`nmkfv)@Z?fa;@y1Gh)1USR!Lx8u;3?Bga zM2z_lL5R!8(Y+a%L_2y_kwXYz!kjsCg3q2knO5Ec%_|s26T@euxF?-^~iQ}No(kX1X8%7}k?ut>MXg?MYr%hRM zD!Q+t{g5R*X9u@i2_aWJbityna}6LF85y~_H4E9D12<>JIs3V7_EK6`^z5tzV94-H zNJz*pSFc|EoAB`P$s<8*bnCl{_S4eP*duIl!`ruS-$_hNOy9nJdp5LuBpsf?kZtwu z9Gn1xMCb#TiHjC3n!jYpk~u&M|EED*Kzd4n*wPCN3vX*Qnmapp?#xI}Pp>7O=CSAL zXk{)ZK=;Ui?s>eYr{~klmoJ|=Yu2oQpr9cCF(CH*+!l~KckbNH%*?!V?%cWCyLa!# zeRt8wKM}*{;b4|jrWs3?01O$>1Nf2nJZkRTxxt~Kp+m-xA3p>V%Ha?(yxra1)j*cP z9v&Y3I=de(v4SpXT?5Gd`}Zqyb8}1b^Ybgx($XqYQ&VxtH6FWKObq@L=s+jcdFHYp zfCwo(;bMX}N$7CH%YlH$C!>djg?R!wJZ{~(RYzPmE!c|d0k{sb3hnV^Z2KD>A>_TPBLzQ5}hg_jPo4;tV9PS3*!XQftA3VWMO28|1U56zcn57ZY{74 zKuk=$nx?{OHfOD1!@eQZuiPJ~QJOlmqHJcCMaDT=#kirb!ED3ZUxZHBATj(mhxX<2 zRem8QPDf7`ZcQ}wjHG*{O^`j9d=6Osf-hv>xnGYnUMcf?pbhXHfTZX|ud}NMEy5*0 zqRgM-M*3Fet#am(w@5j)Eqy`bbshYUht$T45;PeCEat}4HZ^o@sbaiZuXQxa8X4*G zQgE|(#T{&8WRRdeIqmA~?K$Caq3l_6m0#F1-1nXPe!+TL7LhE;NyC%|m=mIvM1Ssax{gNxLR_4@M8l~R9bTdj9_izChB_GSQ5Np5rrwBIruOTwzX-~ZRAjBWfwOH9wSaDmmJZ0De= z%UIT<%pcM~CM7cXiV44kOGzGG+1fotli{PS!qEX?Y4l)v1jim;@+|%wqsM`g!{29S zcG2ed;BD1a(t3OxBqpX>8k9G!M2~MHVW$-q@jjh&xR6-{Hjo8otaEW7f@j7T=*Y>g znZp$r-Jty}BTPA%kW2yiK>lqC3-!t6Aws9EIp-lOnjd3zzl@S{P3FhQx!;|u94quB z=Q7zhZGqq!Svm9ws@_JXKsikpg4jCHD-G7`XEj<$`PjaoY$Y9Dpo29WoIR6!o)Vc0 zULhG1oCVl;$e4#Ve*1Qisg(ZdS1xps=;j$LkoDbnB8oR|41a?9dE-DYXWpA;*X^Tg7&=(m;Z9-61xR4VP2R$jxxJD=wI#LCHpt8I+=p@?hy9r)!flPv}!F z9>5ENl%WqphkPzZQmUy<`az><1MTBW)5o}`02MqjHpB3`ccMxB79&?BXL(l_xw#RW zRY$gk#r{3y_LrA*quh7}wXA({NjMvqrP42F2luw}68A%EU|qLfz_3qV@kVsH+uix4 zo<^pH1Y~{Z^+U1(%#Y|4w^7XSn^Pqa$J4@E6P>pgDHp2;Z%Ho5Z0c-1nq)NpF!kTj zUh0$A1GAR(p%(P9pO-kI-^t=5>o646YT`b}fjXhYstZL=4p297_*z-qtU-pqA;a)A zW%HYo`Z-}ih=4&WxQWz9#cYkxmPoPuCcc;=gru92)-t?ZJudh_;d_kcGU!ZOBIVFG zH%ya*lOZGO1)hCAbS+0%L52uJ(c7A+R5jM-0T(gT3lx=}IK@SMek#PW&xFkbw&92i zqX+?Oc*&Wo2>L!?YQhMko-i`N4xI%rGvO8D7pTyPSfo-+IMrj;R-M`agRk0(m*mKI z^+G~HpY+gce>{{xFvS-eF5O0}1G6p0y170D(Vyo(SHFs&A~|BHQna>z0eyYXQ|Ok_ z9cQ_Rg)(HnV1L+|GSGB_dsxzN4Rw>R^-J7E&J{(2itoq)c=(J`BV}^+n72qvS?Q5O zr+^u%_F-K2+5!WQey04Ue6+Ta6g@b~U!=T9NgplvEei_@1 zHmQTI%vy@XVjVQG*rf=Hg3zc{?Q~MHqx_bq{cfo>0%X=jMSWt#Lbo`w=_^#H-25o! zoWLZWtvZa{|HX--FTx2l=PnKW%rAxVI(6_`Q+?s~&tWLRt?&UL+mdgfsKOu4u#5Lt zVps{w5l`j)WP%?W;7F>oKIeEuYJTYdW6O`Vx)gh@_QTk0Mj@bAELY?oBSai6L#x6kruBuZJ=xJ zGqJD{zd@)UZHyej3Z|IEq4vZkY`Q)l=au$#Ka=?afmL~iq6~enTq_th!+Q5S$0E40 z-!uzm7umAJkO!7#Bd-yqAYo40~ks)XB+9m^&cv5dh zi{^@q{CHK>G!^V-1^`F-OL`pU&CRo#0*!(b5w0e{5s@Hqu_1=YX)W>j9T4ue$8Szc zYiCHBl_8|)pZa@+=Jn@dlkr=j@b%HU6KFS^@+*8jt0-l7Ho)ZiOpqwDiq|qt?OAWt5x##da=3o&HUGHgGZK116Zq(L z9v>LVW9%tJvFabJ=XB&c9~Yo3=;^lz{f8fhY;z5a0)Ru>!YF9Y9ePTCWq>+pNOqfN zg(R_E#f|uYlsqsr(twtmOf&yGR3m?O4Jb*NE7{@y!-bR%=L5;Jm$4%p`w#JwZmra^ zsO9bLb^8Sl9_guxkA@?gs@SwWeCnYRD8Eqp4#VQ4w8hn`2sbJao6i|~dhEtGLk8`; zj*UGCefyT*Dj�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 z^H1^K8)7@#d@|CRyM)^ZtMHGw)BRr=%OME~DD)v1iL8GiwPzC^_iFVA4Fj@{bF<6q zAqAEC-Hze1p-Am91FYMW3V^3F^?|szJCu#R8XwX&4gnFf{hVE8zNBJ1LXY?FF_gT2$37PO9%HGh}=M+ zCpoLf4ucH|af#|}SszIz2jT1OONZ>}zT2TGSL3!kmKN)4ciUdJ9wr^<@};Dz+m5!D z>cLXldXn>au9**(6vVD~F{WQRLL`6o6t1$4aD#TxWM39$nj4<%LxSU@w++(KZQn}V zY(Ax;1x5L-xeOH_A9G*C# zu+$f7vc|!`puavpQd*P_2e7Uc9}8S$Y)bZ(;6vt&U$xSz^J~QYYG|(J-_Ak{!WI)Q zS>&b;1|o;+COcp``|o^SO?c4H%=ec@Lq&Z?t)4k*~()*2s2o+?4zZ2^O z|GR#gx@ZL94^g5?$(wn1pKZ%)k)*m@JNG=y^xotm#dE~Q^*<<3_`B*{Q6_Jb!Lp*D z+-S{3kIN4h$QxVaFNp12+nJJx9*DB4!L+&k%eh3QmXojb!#@IBGg4=vJ^hehX%gm2 z63F#W+`c|fQ`n;WUf=Ccy#3%@gH#c23oIXf{Qdg@jnGA1ap0?qBhhn5;{RCCGcwd= zQ?%o3#>jKl*s`9}*67IA?{}P^+T3=v%>rkJ!_=v?a!IvUuKf=3Ntc)P0_J|$77&1m zcPq%R*aC~IC!Bj685%?zje?tAQH_ZAT$B{u^A#HWv0i*Uag)!`h z2~@s&S6G()fWQmocoagQX{L_PXjl+F;Dn)>l|A+Y$n?i&r#vWx>87cY`64z&QhtG= zzTx_X;EaK@dtTgS+zeLb9yl`#kEv}r`N;=O^dCZ=e!`6;6T}UJXIM6&HbCJ)5LpMT zPF`dsBQUusK;OY3yUlg`j5&2CZ@2+vdHL9m+->`_oSTBij)QzAuUJ??dcZ;aFMVr}kwP#;ZEQm58D<#?8z&Zb zB9V%f@|{!S9mbmB2ljx=MZ6P9Pba@$Ed9_=u_)K5C$pxUh8CvlZWgJqVb-l@90Nk# zW!C6+7%7ecVA-R6#e$-I$ z6CbQele`Q)n_}%~p}hD_zlZ1G!HjY&+shFoM$WW+eN6$?fYg#C$pqKBOT<*CeX)@s zX5DhR*CMjdTH$dtwI_JTr7|1)^s*-{+%N1luuj$9Iu82gs2C>Z_$kv4UsdUJ>n|Ji zBwWzaXHNT+`uh5w#C68}U?!E1$JVi<^AGh&pKpY|xxGFo#CrrcDz`O4bjr%-MZs;x zeB5nnMWCEr-DFgnS8J6^eeIPckQ~Q)0Eo&@|K725Ox9oKf+Mfvw?{gQ2+G9E$A>Pr zNm;PmF}Qcg22CP~z~FC={_HkpAXPc}AfD9XjFn zvh_2&_wE*BHat0zG~HHb$VLI4_iq!hR+^H*kD`gG4%Y_bJ>17cGtGZ|&ygtuZWd=H z4Chv|Lx!V=e(?j^mIye+VfMiv# zym8;oON;Tyd!0(PEjRR2Jc)9Q!vdwFkr-wcR;QSjagphfydL&# zt^deHp)t*yN^%zqmsq#)M^>nwAB%EU;p}Z~RR!gL9(BppBJ3$X;T2t{^b$GhpRvP=FJxcKmY}UA+eUJX5VqA#4GD zo34_1AzT~&dzXK)%f$Y~g|TTFL*pKa#A0K@0NTv*?2CEY zA4!#>FQ$5_4tl+Wl7c{Tj`_z%Cxa5gz}D{~TzU_)mLJvRK?XGf;^$uPWR&1ekJxwF z$Ddn$|9t24m@^#s6k>ITArs93y#sh)Pt2Mdgod|90)4iQYu?$JySsbk9yD)3&htan z{gS7VeRew8jTAgb3>uuXrgM;Mk{a-<>-({n26$An{9u6S*K(gyWo+rUsz>g$Yymds z2X}!(Jpln!5X{mn7H-S8d|4tthU7eZG84Uh^*%k(5d?4>7ya6Lwm6% zTG4Hm4t73zYalN2sReqj;n$GSBgs!(b5^E5`xdd(=GkMC$>8oqd>$&jE_C-WEBCMD z9G@W;n|q7S_5sIaFBscmqjhkNhme7NY`Yi)dg zBJnKr+i1mJ^pz-x4)}LE?^Cd+3wyYG;gsSC(PGAXsiVdW7qh*E*fUNTT_WPMDUp2Q zkMg44miLdA;mdh{PGbl6sqid2p`e4}2 zz4GvDYwQ1xdF`-8A@)0{=%Qygyr#fUd@=0xLhRrhIw)BHLoJCzKk<3c;Y+kzplD~O zFh6`4uWpEsV%hH-f!{RaSc7d+uRMMzI5^9 zsdp4)=B0K<=Ag7aCD8?a6jPxmr`dS|1M&w~!Wm;%sr$J2zs3K%j%oGPg$V#5EcaxH zd={#cVYc($e(L5cPEEZOIMt>5lhL4~3;!u7KS3MqX@1ZQjRvR!h*AM$x_4h5;F;_x zTFHe`>w1<&fJLxR61x)Paq`sKoU#{=d%G`y#Pk z$DCsvkA4C0b95g@pI``^h=xzF zu85t=cs*(7^)o%h$F}vS{dkxz$BUsdMZ2jg-xn4W4@+l;Lhdmoh?n4IRutr;`mi@q zg&}YdsbPuf+YvyK14fHQhJgD-u9G(+cZxUgz?p%w{Ow6wYBGxm5TCqL+zyLc0D9vnqqWACOi-#r`-yfEqAOj`g##$ z_gc0rK60RbmOca6meHhE4TxAoxk&4QK24Y!)4CX&?->gT#{`cpSPea|lhwOKer&1d zUb;t*LbJtq9@}IaIBitv8YIY-eB!3)KRc%`7?1&VOUNrKtiqi{e3BXgn@vx-0bI@b zVO7mXK0i$VC}VX6MSZQUfJ}cqtk48Gr(187o~A4mxDA|G?ScZR*}pFEg6OKF#$hzL zJC8T5{lz9j=S15xhW8z@0mIO$jn5Xx>?5V+aU`EeQUL*2LjPIbOEo317_y@=Sm%rE zL>k#)wpL?T)m42aLuz`_SrM^5vb)O(aSoGVEZ2B?Vm*C7+wlNHx1>yu8dGqKh7Xw5 zbJ|@!etS}ra>;*5sIBj+M$SZo+uNQwQESOa?IGzVm{s1Y{`_$M;>)G5-NV72W38Lh zFE3zBP0mY<<%nqAc7r=VK})}()rD%Ul-6*%Ai|g|D%)*#Kzb#2)}UR>t_2rwN_=x# zw!=enU*gSULAZ9PtRebNc#zQHn&JV)Ms&QE^IJe9j~i22D7jxAkX$D;aMU|Ur*X{8 zIOjv?zo$Pw5W}*pvK9$i9=`(DUZXX#{%DH{}$!e+#)jV@831)OL z)a$54XHqdSU)!yNLO2xUNhN2OTHp`n}b-`{a(@mr3U3Z1`2gC?8xT&Lp+9Xubva#<9J2+IZ zhmNnv`jSOaIQkPaD3t0fI+BkPvEx`~qe#e|k|4SCTdXIt;V%OO=Xh8g&Ml{YOb-r} zxjL^3ybwv>Ijl}6BiKuZs9YW`thttdxRd?!?QL;P+hBvr0);Rw;W{~CS074BlJQNW z+P|#i{Q?}$7oN3IpO+ywRi5MlRdb}|VSMCok{|LszAE3+H}2}k+7-FNu#`CN z0spo0+1Wt~QvX$nNSFuzt{X1Kp?6P45@<7K6C8{YCYm8;;IEVqT~`9_nRmOdM6VZ% z^$&z~3UMAb@LGe28v)pD5RTmgY(qAD)spXZdrB4)cZJcxHcy<2N@7de$0OLuvb=v0 z5r>dMPy!*^TlWyWTgx`h_v^UfV4K=|_&kps4&6lAqU`lP#hP|bkqk-)wsTCwUWvC)Z+!!$`g9Odj(v@6oalxp3%xPRS94jYBvog9+LM6v!cemfkxS zk@{8`6<7&q7|;zfr)5#uU!azQkB&(r>n)-2I|o{WhdLo`<76wIG`pxZI7x=J5&~Hs zO%#_lTaje_RT$D?RVZI>*3#ZOK;9YiNDOwrbBm!sry~u@Nj31LgPr9TF32J$;5N@# zx_rv8>g@HUf<}(;&Buka7~w@81D~x+Poucqea$H`mM%p;zRB=AUcWcjy)LnaRwr;u zTeFSS+Fk#??NG1{3;eA9oSZq<`t<3*(}~C(l`Vd7Y_B zQRP~Vg&-)l8^ z{>G>jud8{}5P#_CKe@~iL!u-C3qRq`EOxm!ZYfj#Osanl=Cb_L6rD#SF)@p|qHye@ zUV*jVeTVu^ZhOZ>gjk3(w8t-RTpoyKTHaptqn`NE&|#hwPxMFi2JqcF0=GZM9zMzV z!daTdu>V?XgEWw-Y2@tZ>ut-0xF))jXX!t0kDfV*#|0!1&%t&&0+xUMx@zSi{ufhf znz}Y~jDOZakdtVTOaf=_DJ+vPq|cA7><0_V+mly+zs&089lk+Q?TCA*gd&7fw&@TL zOXm2t(X&U*_G;?gZ9*N26*#J+CCZ+VTi_$xPdJXOwvZr*VwaX3xmclZPJg|<*yo1X zoR`JmO}m~;fR(*QKKr4!)W#b$V7WN~K%EJ1lo5H;)Kl_Y^(J+PY5=A#CM^Pg-4W=k zJIZT|T?)A?pK7P`Ph>~EGG0aY5Cy8bv@;Ork63RQ^>nYmJH6!S$D)U^btM@$X$K8X zS4%&MsLRd{jnhD#f-^q5nY|=GdKBN>;Ir~`W#&HBoPELQN;PBt!FkQCbKH<|jlxYVVS7z3i{&Q^TlD2@9)TZ!3%-X}Z zn+@PWZXk)+Vc+)6=FK;p;y?Ei>_Ct3fqq2?>3FzLDLldd{0K;@R0*b5_nex#`z}*d z4}1Ok0Q}J>|$|l zr}59crt@|WrA_aTO77u8!aVN3zjv!2n-MK{{Oj-^XVOT|F(Pxf>{W~~Yw(DBrPukk zJC|@CMKrnB27?W|eRWB{GP=*GLCW{u0bm;~Pl@H-IGpn$+PGn9)mV?A1M5fTxp`2x zZTs`vwoiqQOvg>zI687@KKM8MvkafGH$KS_7kiG+{Ifvy@I`lvh?E%YH3Mi4z#~S? z(|&)ewOHv9Oz9r^*pgMr$n+d?PG@(B?l&L+V_r+cv5C z58$GVQ6@Uw0InOx2A)RDE;WdR0|ZW2Y2;%@36IR9^b#G9noQZ^8)fE7H2-6RLZB6O zU%?IKrnVk~jf(!9=LYn5E}oh;?VZG3Js;k4twXUqlL+{&@Ih_#%jFNAe-D;%*9$|4 zor-*ykgi|b{UC{W6H`APVc;((@nhd%g$+e;Gk6~ygqGvml4Md_Q358f;bmbQ{}(Qa`mYV|v;tUS&~2681_5$k(n%j=cto54UlUsmQ?Z_e=~VRNk`>yWx# zdkFisulcedKYJ#0Hz9@`O*Spgz(49nj(ea*+M~-e!G-|%uKo+m?FK{U6_8!yjTW&{ z?{4VJ686FGi%fJgm*wY^_|89-g){GuuI4nlID(kJA+T(Zv5lXr?3cfptccde2QIr( z)cT<;Ur*BqCaEX7@>G+S&xCS@o_&b^46ssrt35_fUO62~t{)PPV>1rD!&rR~3N*<5 z5ID~KKfg!QUoYrrt65KvZ7Jz#fczi(^93dJSlRxUCQ*s8r>?NMu;zf~jVE8$)>Z#4tmqN{M|rMB z92f~Rje*7elh}cJpV)fkZR4EsWjnP3g4hn@lHvP_pJ7{nC9LCGPF*8RuZ%z8sLrov zI1PitwlDXH{X}e};D7oWNYc7~`vW$lmqEea5LyS#Azc51qhU>Kz(V`*OTNOY>Ds9> zaXZU{t+n53N)Uvx2td)@0{SSIW$J6u3CFvfKN}b==?U5+w>z{4L-OktH8IjsN65S% zrkX5=&1k~g#fSZ(3T&TFHX=5A#^obKRw$1CMzqiifzsM(`Hz!gJ74!hvoaT%E5V9d zpz!YcVTEsvhzkP~e|wocDGrl|^!>>o4Aoqn z$wBR@rvFE4e;U&Evz%UNeD){b?~TFRzIDehX4I{&RP)@nEfXhQLxtc9zNz0knusxQ zQG;Oi!<_VDjGkC&Jn-kk*S~cbc*;=PuU~)S%#%G zMc2ps&lsV4$WOeL2nb3TrF$GEK(Irvn=ek)pdO3rc8wf9x_U*O9@sFm2#$)}a;7g$y+{+6t8f*Iw`DxtF{S=B}>i#gF~9W8Mj{B`2#= zKbwhCLtfZCt^xaTJRntpw0npEIGncbex7tvJr?;bEo3eunH22sRIFM~{$McXuOSw` zmhL3H(#OVjx45>kWo}Sv)$$b!jQB)IPOvKfo%P_Sjp#ey@?Uvs*TAUb%oq0NrA!VZ zq;I5##|;M+VF+PV|9t2mmvpm?vk3%6hRlGqlR+P??_SWZb1!gEihhu)DSX!JZu#n5 z+O$m(*6sTd=W9(r@t2ll`KQ@fbOOF-;%xJI@$G`ncX+15_~S(aS%YPgb70e9<%Z^L z5}+o`FSGsbv~Lm%xh3ndIBlpvx0(sTfgVN13APBx)$`PPJcUgaD7_KQTTfypez|R5 zVDuPMii95Hiz^u)+|w()ACmVS#~S)mw(R#>1po7wU7t^0I@VS*YWP!UY3sjkAtBo$ z5<9{~Uo!|-l%(3&`h)iJyQv1*V(E^ia8E(>h=7~?;SsUV@VPO6cIecGpHMX2+bpbt z?UMXyCjmG8-kU?%EC`^@J5Mxac9l7+`y=Q#Cz~Ge*tk#DbF2RQd(7VVov0+HEW(za zT4au{^QKZe{P8=fg*Or0C@Is9J~moLCLd?qBKz+#o{V zUkZJes9b8Q4oJ=z3%8?31T(htKp*AtJ!<)ObnW`oxL4vRko!Yn!9R$xh|&dn(U~IV z{3+UDpVH_+g3;&WUumIR4hM7Zx7bx-bF0QN59WX0*OJgc9cQ+MrB{H+1Cwuf6tbGG zSty9Y;uSq`gAaHVL!d=}Yj`#L^W11pu-Qt9nlc$Kc@>l&A00gxC~Rky@jJ%GJ;<)Y zzkKjGuvwniu-Y`Oir)2Zc<622&ZNu-BKl@_9UW3>L)?LdB?7-;Ogi`Q6J#rpem<-^ z$p0}wKe#+=b;~SwjYy|#&pM}xV{zb4(U=dxXDL9YfJpp%os{73jGT?Kkd?L9t<43+ zvOTI$J_mP{!v~iI)&{*kVuA>Kw5wrt%i2oDTq|KncYR$X-O;TGk?smlCqDuIxS6*XuYwC1Fu zh_`P2vrapFdghT_a7~Z>r-v?!kD8q2wdAMmAVz!@J-4V!<)a2bw{RG}wRLxnEJK~p z7*58(JCtjIy8AuKB@{R0{Nzgf1^a1>bIjz9a86j8 zh&kyg^!>YRl(EfuEKuP-$S=$MhWUs0OqM1`P{%n!&gAgzxbX`eMzr&+%=4Tx7OInA zTjr>@e4KqMa79KS6>QU^#op-kY%_4Tz_yzz5w6QRPcaR!_%ZkX2&xky+t0G8M5TQ}0s5hZ1?mXZdj68HZ z7GrGkc{F!Oh;?V*!3^GTOiciD9a*|qwUCYYHmY&owq!%`^%Wl6|4;oR$9m{F5Pg1`DrWR`7=5PF1me}xm^FwWg#riQgYPx}Kdj|lA-84sJ9v_Exv(fW1wffMx#*!@*NG31-J6u$24 z{BX~JRuA`|9F+19@MiAcYxP-zX?xeNV`(j3@J%cM?_S6cUsZ?O(cWe{4uO@kVRJI@ zVQA_8GdP+ zdueNl#l&b64$Z0M-kX{&n5p{@pXsA6EsZ>%U;fcBGfir2v$$z!we@=2YTMemxEX&BQ3ElK9T=cI0A_4_$SaS^hso+IK88x zq4tYvz=LY(U0pQPM}uEQvVvlHqw*h=(Q0RDDC4B&GkeePV2k)LXKLq-4w|mEIDB~3 ztSr4UXZvqY`rhp!##>kU8H&E%#^7HqgcH)_?n(@Kre%1oo^6jaLXoH+k%^r&`o|Tq zPVet7ue@H-=yc%|1U$ZLxyHMqnJ&XoXs1-SZlVUq1?dWLjB?>wztRXSxS_n#Y!PLd z5qNiYla`jI83j3WBn16;6qJCw>a-m&`d5SfxQ+_QuC(Hd>Ct8j&+a9a8T>y@0GAV^ z&vK9pV-pka7wrU8UZqbiECeP7br{$IACBks-I9TBCZ1~hNFkk$t6T_hmu#^Y< z_glcy@Q(w|BXMzY0R>TvDQ-aWT6<+hg`*tRP}jq03svtUKFh!8uA*xmii?Yr+Gh5g z*jZefn!3>8$`1bT0WV0WpJB}1cDYhD1b(5oOeU-_D${{kQw#)r|845Wo|miW-s|az z{M_IR%!w2zU0e_ImZi0|t!>E~FMkB|U|+pg&oIe_%vI>-0t?IssdH2Xftv?jBaRvJ z@D|}<|DO&I7U$+A5PPpn(lw<~g|7fL7TUOQb*gf z5CirxkFIH^sJRB1|Ime%)-Te{jE%Vq6%KX13i{uteL-_(Ai~zl3`#l$F|#dv%X}=M zMYp?h!>2NHbX@uczDRe z>Lca)}_L%Yknu zFvZ2i20KXSSD1*W?+f$3fJP@0Rm{46uW(6XdHk?R`V{bcZ<&Ypjx-!c@1vptCQutt zP=TqB2=)X8M7lPn7b#c^U4w2u+v~FH0$h3*mR606nki}Xs+F3?Qk{;Z32FEPq#YQC z`(A%AyL)+mbIy!qRMbDg&#UqosD2XfIevV+PnRoNr9GxLi|xKH;cN|Z`N7}5Pq*2Pe)(-NeNSz~`F)>gUar>XcvT?F zgW3%K8Tiq=i}R81@wfIIp1fki#vVlWUFZ&?-dG z|7k_n7G1?(q9^EScFYgL$S!W~=q1-4)X$h5#;{WLY%%YH`R7}^jP=^pj+A~0ntEon z*4>-OQZ=;LcMbe6xv9vb6tHMF$MPmmuTM8&xa`~FuFqh)8otjN`cShiZfnmQV;}DX zD&=~8-}!6@CU(cfUSfz0OG@78^Rh^(lfxB>0mDy@4gQUhN4=eG}`w`-=iT0o{=j_va-YsNtQ%q4$c zNp1z^;stoi;}fsx$%-Hm>cIty50}S9JHn~S)qIf=ZmTh%N{{qkut6@PT>^wsOhh;P z`I0yolES7pqTYaygt8>+d@;b;!2{HM$_{28_Xy7k#%*ufySzMaY|Q_Kv25?uW%}h3 z#f<6^%gNpLcj!Gs<@gY&zXUPJFUK@+(rem+5kHYI@ysKaPkff`SDn768y$})X74$O z-+HyK+%mW=GdVghqFs~#?%RSI2wZi5`24BRe&9SCcS|W< zdUrTOc#}BhAWn84s&?749JLYbsey7?089GArykA<1h4EH(J_)kA09lplVa>w zcY%5!zfy?#=H2xvDymSaGrlagQ<$BZxYOrxRHB~&EN-nT;o`xwu*`4MN!oo!R4{?{`y1BZaHi(^6X7)B$su*?c$HvO# z7`yzf6ll0}>Y~SUEqs5kK|R{I);QShcxPN@c++pURMb?jV(jj-NoaJ^~i}2p{UK-Ko3L%t94m|BCkvX={6W zqkYm2V>|l%=I*9gJF7De>2d5~?z#&nE?_J=@Hp|9kjBE#|K}Kw(#ztpGgG7}U6TRe z3#|jggnoeHkMlwi3ARQooubM5M0iKu`4$+gJ2_s7FI?sM5hs4_xthm!`vn#-^?+x$ zyNo~|KKUG?^7Hfa{qTYmpj|lkzKwp|IRp8^U2bmfdBUJl%0p^g(`y7!Vw{x|rY09y zL27@359Al`O3Xgj6%)>*$4WWjq8LTaY}fDpp^&SMi~Tu-3I~q!_$XL{1v5LwEmVG* zYkbNGH!W^b0h4JCVMj5B!44SbV`y*%-Lv{bBh5bKp}Y?ltyqp*>fa9DG*mMJVzbXj z|B#pFw30?UlfB?J9MgwNULuF~i6(VnciB#9Hv;jXBPZC0foFwbRnrN*%9@{#kF(h| z)6O8>f!1rivC};)9I!`VJ)$hYjC^ErX)d5)b1rRmbXA_#ObWd2$*AY&N-*17!13KE z0Z%UE;v$cjgai$03`uK0^M0$Wm$?aD=G+$0vy-lzVWq>%5CjA#;UkgTy<{sJ%S$DE7@F zqgNylxT9}Ei;;lrMdSMmu)ug~35kN!I++h?W3YE0m2t8voq-z&g@s?tpI~Xmt#9#s z2en&P$lPZKAi{l#MY5vu7T}CGVgkV*i7`doh(HuX@ZZxl^(EV z_>R5ol77aKly|8HApbsEaXoNo?VPBCm8PqTMXNmfc+(!==xaC`)`j)v`RcYhhey-=sM^y+Nd?z_aBUGAqqG_zt{}<7PO>_5nKPdPPMM!83OWg& zlfMIM@6MT@ER9}}Y7M}a?s$Rg?&^UYft|aBJE#JK;K837RoG;L17P@3TO2F!FkEON zh!=7|BRH#T13S!?1Fg2-7rt3OWy+#?``SJFm8CB&Zqmzb2T=a7;B?&Weq;fVA*3Pi z!x%cpY<3tnG5qtk*^ia5Ke`n7absEipe*-=kWSc-DH+fk>qX`@XaOm7P%%G@soY@H z(K0D1t8^<@V}|9abrK{sM{RZfNmM-+9^x-}l=+EgxmV7qOL16QYA(U;x0}-!+Mu|b zcc;|qm$!q`2SvG_w6)rqih!*Oc9 zT%Gq}bgppyz8DQ}ceb`(?v?ew*c*W+RQ`}wcmuItHLftql=!-~oD%Q0qiJdjk_e9b z?ZMRf`%RY_fE)k}g^GXBe;%3^mA|d+lTpJIk+}PtYdz@bErKFLl2 zP-H^4NizY3pbjNh6|nJha`-0CYuZjZ!l2uL1AG?C#~SDi@}RUDIb7IqL15Gd(hy9x z@>Q`3=E!zskX-ySLt%rnZ|NN;I?nYwKIcqW?(uhKKKndoyG{$o;ppfV+Wsu$G7UHg zyo@rT-1sR$XF31(@3k%?Bcu0P?ETN9qoW$H{n>M<sk?_r0do8v%VbN3DbWA}Dh> zvD0bd(l@=p{2+ylbnu7>3Vj3?^jZ#;<#wg$I08Ib z%aBmFyXDoXizF)%kN^1b?%n24j-$sLGDZ;H(Kp!b-{D`0``e-*-7re> zuVm9Uw-QD!-BSx*?PH{w!D=4xsH4O zZlhd-?6mD*@FMA*<@2(8s4wL?%-kWK9Z?wwj03rbLA$_6CBa)+^1b&Cf+w_cOYzLo z77Cl@z+ZL_YCrOyvcFnXJCFBeKDL!AJYl056HTOs4=GCpetZLsT#$ORaz7J1_V_{X zpU*%1c)}ETrf<7DRkeDF!gO>kP3Oo3w#NJ$>{4}li9^wrc;P|nx#)W3DG%FZ5m^u| zKp)Dx#hdGI7a=(`q=dzh7VXg%%_L zZ%Oo4g}0n2AkpD@KbQN4UypSS6DA6)xW;&@WcVq^#Z$yG&1P$fI691Bu+BLdSK%R}m(WCJwe>ry~F z3ZK%&VGNaqFW9pdmjxCM!cBB9M1q1U#h;g7CacIl&LG$S$9kn$G#Hg$!};Sf^4>5X zKD!bA2Lv}*xqoKgymuXcksyj1oVKDp$Ssy=23_8i& zeW=1hK@eiOtjoo^e!X-^RBdn0&s^E0W{=latJILT;G6$pdb*cq7eXUVg<5aGp;<*OXi0>+wYpWjlfG6 zWGI3hFIAp$UZ4!`l$Wt}uyGjiqf%7hqezobnJFFFBF@syttfQ>yKHb?ZVZ)ai*vtR zI9K6r+=M49{&GDTX-1S1JK*>Bh=j!aH&+ne#=b_wH5VN)OmZGZ-)D`ORlU7g<8(#x_u?L;*LYASL&fO;xc zR~TGJ9;bsfR8O43x+^s8%1<|b(oub1H9?ic77&3KkaYKcfyD}JL5L32%ixxTANDR) z(HR8_-8NAAr^tJu$n|Gfc7aGpBO+%djgIP=XTD!6@4XxW4(pEE}c7wqi zd(uzY2^mm|2F-k1cpDoaJnIItd??rL@fNkpctMyE!lqF8fD&c*+*AMrR2$E|e{nA1 zW(9MaZKz?HaZv$XJt{Ar{!&QbR>z9Yrf~A>CenojFlGQUh6|vy!zB}DpT->N?&PGw z9pI}Ecrsp~B*l@*a#V%1zaR@z2)}^?k}@8T5B%7>3~kZPL7E;VpNU%r!~^+Lr4G~a zzv-RxkTb69xBXY#cHupc(-BUJQvDS>KfkyHuX?{o-jcLKm}chV_n*f_^hSkpZx zJy0zMZ%_1kkgLdLd*{KkQ>WiAz2EZR=ddi+IJ^*VKKUgxJMk`9DpgXI0DSg*JfhF) z=ccnF>y@a^mnE{!d3eXJ6~CO?f_dycS8*vp5Gp>5)K5Im_@l?2T^`;}aZ~)Mv3ALGNo87Tp(Y*)puc+sG9G$)6cA#O1qO#Qd*0rb`r!++`gA zsCOrl$f;HYMBh2fBim^5nvNs^TFdB zwM%x-gPUqHaq!T2Y0Sp2KuH#x~SxRFRQf^q#Z=D(Jo!H05 zmXya8$Uzy{Bh2+_uez#hUaGL+=K&Ekxtk3^QM!Fqm0Is_Hl7f1Q$>eAD`Jp-TISQu zAE#yyUhk8pSI4xy7oM1XlZd8_1$=C@jXW@!IhbwSwFLhSc|hTk_r%ktniWNga2ktl z>*Ydx729KFylE)p34P|WsOTozBi=yVNyO%JEy@gYV>j7K#+nin6BnYjtHo{A-~$Fb zFiKI%50lqrlcyHK(-jCz;wtY<8rXr|9Up0tF2M|<<#_YAP! z_sIfBPP7)Bf8!tqXF-bKxn>tsV0Q>Mp}OdBI_Od-v_(|S%$75GSYKP@i?Sk_fU<(? zWk_2Q9sXy!Y~mJTpMqf6DQ&lGo8gneAvMIT%9e`_6v)1<8KkZr0G71}5(Q`ja?{XY zzvpw;nK&t7pLBXz`b$1Z$>7Bf()LG7ls#h@bAcGF_J)vOhuEQN{DJ9T=Lo1~!#LW1oZxi#ptc@#^BCE*~(U)l$gbw;_$qP)v zHIh7G+n6T;X#VjQxtU@@sHO%tkptxSk%TVxBOXz|9~*(>^ki#FmF?%@XBXty46^%!&`Zw$*!|H_!sf_Rp{ z8F9OCB1CV;LxrVNE)DFH`0w#ZbIhyc-pY$JaG=YFS7gA|;l2$(20eKYvx~5UhjiwH zNh8KR-ab>u;L8KjsLAZ3VPCrQBtY_ys@^T25Ev=LLEGl0CjNmtyt%B;C$2Pxc~xms z9^1Sinbq0j_iu9!p%ptOIeC%Efh&*@ciL=AnaNp#eooijV~SdF*5EKa9PAYJoq`)H z_dxdRL_k2=l?E219*NYAH^s6}oz?8PfA;1NK=p&tr0&H2pH@1)xgB2MKGw(qCANFn z>w76y2ya^+rdkUCk|tM(Do6DwA*U%K8RCe#wbcm}?(ZiLpWZGQ=Dq_*97N~0GK%6g zptCF~LNQ77{Te@(;|hmGv01eRdH+S@&6q~#DSOFLguw@NuW`U<>CTL$m#*M0{c@Nl zrf>GKI!0Ch#>&w?Cb^vcIBWzUf> zx%tQ7Ztuo3QjV;%JT(qDn}Idi*va2V{&Q{KhnaN3Z)H$JRB%4X9@DD?2`NuwzLC)! zPrN%VQ1xtsSi+lq{ViuECV?*Iix=%Fz!d-Zd~J6WVK-$N&WNuu-3CS(HwFJJZVJEv zLU5;|8=__KX*u&Nj1V>i(EPfjP$iY)VM=-GyQiq;cR_6TMV<`iEbvd?L&(fts{2K} z!@$-3UHKT{>_8#>UVE=E!dMvUf?x5gSqf1hT6lD>jNMVWuVImDYLFy?t3(_r*ynp7 z^q{PPZrw+4E#XV><(Pj%2Y(%M3=_>~-ti3 z6cnr}&Bu&pBR;cj`V>~ z^zL)IhB=T^u1C@dF2Mx&3E!Ki{OG zDRr=FwSbDCo&*k@znO6R%Yy|{2)@?n`2{5fSJf^+vJGdnPCiGjqJ8IWV5eDgP1msX zG>h~!PUmj%v+N>ixoSw4?(_KAo?*bE2k}uOzBLq9aqlN!AqG^F@|)e>>^GdP2T%1@ zk5Cwx@Dv$X_xg zVphg-DwUY%Xa>E&7z6eAgiVw(!$8c;b2>rOEddJQ_SD|*%ic2wB_|u@_WFMWPMPe7 zD<6*x^?GvMBSh$AF_eG7{aGNkgsQPDJ7B~9{I}CiY(nbntoZ#;hnYBRnnzWS%DQSM z&7VL2`}x7$49ePaho)Y4`CDp$@w+;RMkW_uyC4Rh-Qw>-uj!83FH;+bGm)b>cw16z zdR%>Z1wX7;NdZ%Mki>?+v`C2-eCrILy;4^pfbvRK8!T0!`-EQa$J)~)`CBMBIpo(3 zVFT9FiG0%6Ro$*r`6RZj!52XA%_->IBAsd?hnS7Ai754-w`Z1z&fVx`jM|5q)q%IJ zdGFK@s>n|o&78(GG5G3$_iT+K@w^mA^yGkY`Vq}R-sykDH;W{W`qIKhuUhzQb! zh?Nvp$kzn2DpALMP@>2PmwLZ#cOG^<~ygTc&TP#Y7 z$)p_YW<$DuG{NAG ze5|i;<1I(V%1P`75Oe6^a8L#wt4>Se4gD9WuzufdQS$7iaoQ_L=OUWYW{sLb1n5HD)zk79T$fbn83APGfUa&4jzAXTJ|ww0Yf=RfaGNM)!rW1FEp` zelx?Nf^Y1B?QhcY09IxS+RG~sWMPgyhi09_VVoBfX_@mPnZNHm9o!y<_mOVd@m$XH zBsIl@lv8l-mMB0O{O;Is#D5m8yMLboW^lrse)A+j>fb(g01z_-pMxRpi2zd?{L&Z@ zbO80$ETrceBQ>F_BfUns+=wr~vn4Fm3FNTdOxGM4MJjpd>*7{!ax!T&TC$inZgCiW zd~L~4^o3$)ZukDRPt2iKA*Vm(&oEyf0TZb2a-b~z zFP)IaQ+T*IN#Kq@`>CX?mQtXXUEOKta2s3t7wNM3-+;2JJY-%rG6~voa#<0D za#6oC#8P8GJRD4y%QUE80#F~Z$gmR`=7gid4a?*9+g&92v>86m~vLJhQW3Wro}wi?`PEal5}##~eO% zHLOfcP1ROZ@Z|1|A)b-J`9)}vI%H$1{w9R^eu2H>Bh%BDs!(Jrue`6~<0u2%2D*fe zC4VzEwdcF`Ppz|1g?RFFH|+Wv69^do`3lT_72TP7vFmmcqmFhf>R_0GFKJhxP5vUS zeKAg)%iW5>2|`$JO4Y@kJKNQ0WapD5Lw zx8(lJdWu!ig{pd0XbRvsR$O#i5`83m8yOK%4JWRVdf@ese24uliLP(99=b7n43O&i z_4W0}RmK@$=w;n%6yOs@kNU~K2MAZXL@VQ@^3TbloS_9`j3~8PrR;`p;;F^+nyFKU zWPsAL_k$tXcO^_ibiW6_=MBu+*)8g_eOBv)fnq_lv6>iyLD?VMwU;?g2H?0cSSF(Y ze7b?WL1K}zuDHcj@Qlz*xSHWJU9&turKo!O27D(rAv#p|LjzZIIV@ixK?p;g`-G3= z*Jal~E-Ds%j#ptf76JY-2gvMsL=t*Qr4iwSzPlcM0IOGrgD&dzKB^Z2XNut-9v;Na ziQV1Zz~NI3d9w6*B*zJEKJ}4c2JkA=w23PEpbTCM3H$Xsl`Wy;xL3F)CzPAz>Q06d zwDkg!cEmBM>WD>jkwkKBPmy4cH!^B3FLtF&f$PdQGF`Da;wrD#ApjA;BxB3BZbugI zVDN|$jazCNK#d*M4}ckH?HAyfj?&ZC4&)2d?|Y)qIJqZsUoJGJUFoj)OGDU%DCK{k z{ObVG#*oF4iBgTDk^X+Xzi78I!&Oet9`ZJnqC6n^jS|;_B7@QVyhL0X()OEig_|L= z0r-JnZ=vndGKF^YS%!l-C2eODn2(1XT)z+oemIl7?q3>hmR$weezT|X{!O<51%{63Hb5Md2gaRk!0f$Fc3U*+PeZU9fYamp2ZzFuILI)0#d zWR+)Nwh(1gEteAOUWr4I#d8bx=}Y)t{?7mQ37Av@H)# z!5QZB;VlV;T%#kYzn|Za9#R;?{%m7RX1h{?qpIZuR{HYi?)BfFl5x{-E@Bh7;R|oo zPT1(kt_FnE`fMh^O!9RSqNrTNkprn2%z>5rK1AiT|uZp@_?_ zt}YcBlFPlnln=Ccqz~dX)PEcpB|Z*xc$)CNa2&9uV(InQCaD3CO*S!1+SrrGl5!TP z27Z)6t?e_fFeINstGhK~-E6VVc~?M)g(4eypU45|d)+v%ownnb(Lre6l$W@tK7<;1 zg8CbpbD75;TnP5*MbrFU)1rbi2vR_Hxw<}pqO#faqz1kUrokwKEwQ`Ye9i?xZFtdY zuh6U|F0LlJ(l=wT*s9}%e4-jwq`|_&K<>T^>b~9r~GRjJ@$&&Fw?{n2 zQ5P3p(*lWkZw~-0po8xWcY_2LUMyBu4W8pRSO)-9Cz=k}od|w-f)W?q& zcxM~g1Cpj)o(0C2j1Mkx1rJcLLJwtc)J3_#*$)fXI7FWvenwk|UAN-ams26n{*$hfpa7P&1q! zTXI!UIu3F_CzCFVf^1m-TIjaNz*0p&_K5|UT- zdYUGxgb^pAn2UR8NLfi6{!#3EZ{Fd+FE@m1!g>u|L$YO#_v*ubE@^-gQT4xE&u5H2$dn-%ZwQ8ly|&_6$QDBkIJa+!>^XZhm!^hW=6n@yq@*Y*D zT!z8|Ahu7+w z*W{}^U3TqW*v;8e;mF%iJ!^huBvX}DGdWYsWzf}HChU6}pKPtypjp1y##>%`|`Aj)%|Tr@-}l8e@tC4y1fwRiLBy9JS|IUTebc3I@kqR4oDNNf}wTOCL{_DSqH6VsN2~o(8^EkZFxBDjt zY;06}?jN3mD>%ii;>%jHiZ)ClE_xO%4PP@+%mg`4n4uRRx=Ey2oS1kL!PYW~ND2DR zSQYkC)yHd;znDt(xO(fsxPcMfo3)W1dZv^pdXhcV9>!zMUl;3N?71OF+|s{yy&qm;`c}vyC{AM$qcj znLonUKM$Tjc{)yaGm0-@U1By^W^Vpn2w9DXOb;QsOGLZ(2HCk5x}ux+e@21vo&R%3lo*J4c!$+;7Rh6*sXzN)NQe{S)ZV!>UUuZ}O8^eppFQ z83bp=1K~L|u-rrF!bIEa=q(KdKnOO9Zi7RfR|Ag2r&Bpvl)(P`QT`ss@NDAmegIYyq?0^I((KryrmYq3R2pO@JoaZlA-SS|@pcZ2M95K-M%w%Z?(U1MkaNx*RYH2R<}2X>Fv8)l>BHa7 zOk=Jkg}1V??n8ZsaIe#ca+u>`7ZL$yH827f)YblxI5P%Mg8t;)IpIt8vkE6t%kgRc zTU(RB-5k~ZOq|@(@3XoGA%9(*!zGzZT@U5^h&?6UjJhUMs7r=e8mdFsXut08bV;fi z7H9hrEHABL_mB-S;7Goj_X~+eW=6H%QgMqoa5|HB?XVL0yu(L&T#ceznTw9UM2Lrf z_?h~?K88AFihUs*r$}t_w!nP}x$HH|E}fx76Xf~{fcmFsUrt!0q3(Iu-M5|;+4Rto zwX{H}_&5~H5(>-&pw3J+uhIbsu24(OIl^JNn5EPE--z_P{gqp$;OUMPR-V=6uHJLq z?oEMvnR)NSKw|vORGOTgJkMH>%;|9mi1p;#@vK!hdC$h2H0nnUc?`DoJt=UN)>2wvm z2F(}+a&6ZCJZ;ZdSXfwsQ%LY(a;@cIM6w9m4t)3r5B!%CVdiw~{y(u%dn=1rd19=9 z*@!x6jAtzz1^D%#5y zhdwm(Sy(XgKC8eT0m>tW14#b;6|6d(8M8dwit5<25eP+~H>UZ{t+>%CbXSYsouo|d zm!Dq;X)_8u>Apj7ebq$<0FCCq>tz6e8NgX8ymFRIkp3&s$URgT^1Y)lT&d+Nwe`YV z7hB2@gr8zPvYasT<<;ks2Oj<{BQfoB9k{JbtzoDSs$>%L_U^ds$0e$OqgEIrhcI|7 zvLGy5q7YSh%z24E_1w%$D!Z<^?5xlT_UHf65!U> zICHQ6p4gO~J#>9m{O&U1BKf>UQHJzMaq^MbU*D ziRHWT&R_>W;OcAs`~8Y3EB|WZovz_7)P;HD5Xj!$YQBzxvTxuh5Mx9^So@L%RlUfgxvQG9ZH{)oY zNW3?)J%7>K?3ij&L`Zej>~KUn5t^r^o9)d!)9ShFz;^wT;Vo7iA&8cJ(`*)v47|N? zf>X4Vjo$utl>U6zH$JXIdEqiwN8EW|uzq^ZPi59|KvuT#+09|d&bnZ&X4FvJT#=5Ip+}%>2+lBe)!UMb8{ot?y;$3rV6Li z;y}X=8j#0@Kw8|G4z-@bS0`%^xT5Oi;S$27K>RsVHbD`w(}#6m1+LTt(O@OE5uu=y zJIbr$hSgD|DYg z4~D^Zx?%UR{@BzYFV(6-p~9B4uJ2JvPVxY7%f&8yV`ja8K2W06Mj zha~jN<$dmD;Ers@DePkA_0jllklzan1J9hWKv>6sng1Ed1dVYkjfX|0Zx7_|J%{fiE3>0v}DAehY^R*8y=irBVf zA7KdD9r?#Kp+&r|c`>&8$OhpN=PUU>xy4H9vP@kMkJG)6zs3(0ea}Vk1ZIjjA`3dc z$JLQD239ht@HgZ<3n>m~B61;WOm-%dyNT01R&wc%ZVlSU2?~0Y^d0+9hLoC_L+I?> zW|~kdM@qDQDi6KjZ8f!vxY)Jq^}TovoaJ0RrlZ|^{I3s%x+b|g6Ne2gz2;rJ^X{Y% z^4;ef*kp*GHdGvRTHXX{Y?>&m7uu7&3eHph0LFBZIOB@bUo|@~+=$DQz3Yj*_dCE| zn>>Z_YXg3h+c^T10Jm-hFXk7ZAJ2kFo&6sgZ<_pkd|LQrww#?nhiUfF)8bVoP72Bz zHx(m^J=f@1j%pw*c2ax^d+B=h5<=D3Gh4c4b3dqFv0z*=DK(7Iw7UE*loJXqBsObB zyC9B#30XOQ>xqQd|93sbW^h z!n!8uRY^9sH{ICXF7C;6b|Y4~5JuzagIUcM95B9qs&dZcNwh%YtgdkXy#?o0v$m5DjQp$_as?~h zoItKSW|B=JdkZK>IDoGSK5k{bRGLuK}MbXNbK5weD(SEH(Qb`!KF-7)QX0x0P9B=X7wC|Dc8R@;;tFm{Jg~5 zH_XwOO+pA`TkprbCSr#zogC#oWHPITT_thfjp;}?n{W7-SWZT`V!yXk94*ZFvuQKBSFFBTeT=7B zpP@T65fe)1f<2FYi6Q3ZxQ*$5mhMRoYll;r+nV=+ol4zx_YdgDZ#Ve!eCMqUj=}&i z<0PwK=6J*`{(*~`Di($q4;`e2bL0d^zE}y^Z zxmClUBh&ci_5OhyLVMKmb2|olulVDzF$WSf{*?X-5x0!-VY+)(hX#yej30*T`ULR9 zBt5zSOZChJ+^#TBEC(kiiv&#N=R?upC8N+2y1#OM&uE6U-YKtq%v3S+AV~($hYgX4 zERbx<{OXdnc-4t0#+mwf5tuz z#yf{+@bqp$qzS(r_{UPd7-bMqy0;L@QgNKgoE_cR#6 znh9tE=GfCdeE`;DvNALI<@CNiMc4rN4!>w57HPS#DC4-z_Mib!y>K>L&`e&?&2~u- zqjzENN$;erV4%@l`mnI=q{vLG-zC>S(I&^Gddb~(X{MU8(R-DIWcbX-e`I>YuzAhh z^DkQ@G9-};JaCu7%f>&svbPFvuHu)qb74=+Mbt~%J!vyBP@Lu$#&pQp(;6*c*&xtJ z!}vTpEJAF3#Q~tb@V8vpT{+=F`!|HFTyL zR_$@+T=-`Slbex=G6Sm*Z@a`JA|ir-_Eg*y6Wb|L1==9iBM!Wy{bw&bzg}(*=V5Gy zPtj-aLvKr|sAXm#Y5;2`?b-nED1#)F&i<9>AE!byCJvzChE2{(^QL#|LrP#cJyN!* z>cA$oZ9=Zeyk$AoUc4wvRRWVbvgE*LO?7AhSe*wRhtXIVQsgl2(;Wr5mLbJauh3CAcEH8Se?xah{ac@w ziDj)G|d5D<=1|wFiLiFOu2K~DhKh8gaulv%6 zJ$pqLYEO%Sco(XIr>RcMjlUH;y;^$*pI?gI%^knjUQc!F<@whh2*b<9I-#io?B0Kr zw7w?FwR&hJL|J!-96HZ7Y>%~@=2H%F{AooVPtpGHI!Ww*Cts$k(@DA}NfUozX&T2O z;ZGVRjHAC(p#stQt3L?;#oyd>p~(aZc4Z@5>gjmiI~X~Lcpe^zlD#4juUQBlug-{W zx?Azs>B^*+xua==>`9-gQRT!BX>P)d*U;j7%Z{RJe}sp_+73H!TUv{Mc_bvDo*Bi; zm+C@9@fo8bl@Vk_kz`A=prJ+!hv1Pd#*kb6$5*#1Z-Ym1q|p8Ym5%X>_Ehp)!2i((=AG&!d@!?`lfYjGkh#f!*l?EbfU&-~0a7akM9o>D zhPEhY!dn>VZRP`X4hOgD$(@jD>)yVhEW+mUxL@JmRpT_k;O*a?wf>0#^_2rzxxVqHJ?)}+fduEB@d5NDD~Pe# zmuLA13odv2L<>4HhVg~kJsrDnn3&Cf?ke_i<2N4Sz1&Z{+#84G+eMy#c5HoH+VQ3B zmhO-MA|K|b_3V)PXMI1kN7Sz(tZky}%w5(`8SKJD3t#+t+~9-GJWfWm-GBZ1;g>X# zTS&Fa4u$(Vg{OBFjPrOZiCB1%tRaYS@!8Y1r+#ITF1?SY#LP=kH&Xfd#RORnR^l!| ztrP!0v>RU~fkmG(>)Sswc)0H4TYb{cuzMRb2^jgv0K38$Fx-YxT$SH;jL^cfmameD z45GZD%G6(_O$Qkw>cMq2MR}ZBPrI4>KJoV(PWv#w5V|PcOFlGwj5wO6^6HX!wsTdU zsTbv0-<9p28G?todAf{$wG@6N9-_Nh0ouaACqdA^j|xLTyBMS!1Z=uXQWb(X@HMs> z`T>KaUVQqr_BLB=!SH-E5h z#zz8Kxw&!nALHY2=_&e)7YGIWYTxpnrMq@6Kg<6~F*M^D8TX3>l#-wd{Oe8%_VbeM zvOlzcQ?&p4*BN8=8ro4nyB>2C98I#ibspG-^N^SH^g!iL(^IGNz?*c`AHrnfKA7qs zEnV6jpt_F|e*}|V|1W#g7hEr=KODhSeTd>CMBEiDtR7*u#6@H!!gT5rQ3`jC@)E^w zJ|awFMTiY=XANjdgnB>nH|$Wd;M~AA zbSJv>GQb2GnRj*BiK%3NGo!@y004rQr{NQ4ozl>MKU-)n`;WEWsqO9-9aZeR5F(B4 z<-nc&E4-Y3!DC>9_rFl=A2BMR2dWSsqA#7ial5y*=1cB5R;cSOfa@c zl9MjSSPf0HXTWa@^om88Kg$xS-?-#vwJ-adOZv zPg!|HgaoKZ48UnS2FEV@p~;T*i|<0%h*;}9IgbMo??H8h5grum1HeN(2!hg}!>?26 zHK^e+Wz$2-*#v--7GPQccpT>^Bsban(}`SyQn^c!GK^jFGnTo=QsfU&NqS~|bw|hD z82RqTUajKAhArWi)z60#z_dXWB`~rm9N4{!8O^Y31qHt3_v1nbAGr(SWbHoWgd_X@ z^){v2Ofq}^i|y|#0fQ^`2AS>6k6N^Mf^S1@Uj9eM3}v2@=g@+0N_ro(f=^IT1};>s`c+b>cVH&R1v zPoGj=GYHuZ{hE|XzmEgc0%OEt;@QIpyCeQc+xM@tr0z<#6x&7x7khyk0b_4gC@Ak0 zRak%u2Hk+S=gp~63DO|~=>QFy1gcL=psEiny}3DLs-XXw_bV<4%T9)W~p45K$h&$8+ zq{R&n229b-@p6t^0p!e|k5bBRp)Tvo1UnA%jzAY{IbDR^0XODM*NZslM;U0%4s)8b zdbfCHx_^U!eg`(E`840X|yp6jLKpuFgv=8z4WU zx(FmmF!(>Cge_eZOB5=83FIb$?W32aSBe!7n8Arl463&<%+%zj-%`0O;`6v#OoJw> zl@Mjz4FDnD1K-jTQ69^Q5rs(*@lltA18JG~N*}{*x0hIIL^?TSx1>ELCS#93p4kx{ zUB7AN&9GCnql$Fr2-s!|>_cF-5q*NKi>J+FmenRJKYb&_J2&fmBisJIXr4-)mQTO; z|A@&{L#>!|Q&JPHU@%YFV^}bIIeue;R|?@PIaJrayGTo4A7G%A)(cqzDx?uwIs0A` zf&duCq#CZpE21(KrdX(u+!xD}fd2{r9?3+gq{;PoC;EM*Cggj3bIW=7TWKO5E?{ng zKmOKhKS2`KWe@Yqnyi8BHIxMJ_W+HCzcuddr6w(L@1u#{xVe^+*s-bSU z@6ERZ&E#tMP>6usH>Te=&sI1&0menTxOkfE+dUp5?3-4$_+EFTr;PRU9t@_};p^~# zDCvPB*9?MI=lUt@oqP6r-F`EQua!z-HxKz;AFAy!y7N(H2q`ohTBH#oL$HrO_2&cY z*m%`p?hy710+q}X_s8&2E%OQ@C;_4&UvGv<7;b?{3q#D~p+H#q`vS(WuMJHIRQUh>f~x7Z8Q zc)?D-mOanb6HIJkeK^xz~9{PwDty=i@n5&%+QPuBq_ zn_$8B1VcYunpReY{H}7y)xd}z+{WqHb_;S?3BZ{(rgfk|dkoDI_Oe1)&bk+Ifrg+R z8sIEb>JQ-(YRM(#N-;u_J>LvL%c}qRJu*-VE!%jT#^{%d4Kvicye5zFefU>9j7_1h z>-=VJ9kFW>X(Q)|SDeBY{+2p2M2(vpX2dCSpsF%&2dL#jdi_*R$|3Qc}~t{-ZiL#2p27J0|$! zNREyj!MK7FOu!|~;#7>s#$_@*NDmJz3rRrKnhD`vz<= z7hK*y$hl5Qdp}#=ltx8H-fc@2P)$Xr4%TT!U0z)Yc$Z}8+>ff zHJP3&D%dv{%*eH{4de3s-5oE(`Ec}E49f*5p&AN^ltZnjU4DE_7+F}<)Qj6#N?eX_ z?jUiV62Y{0Ia8Y6s}fMHL=&FpkCq^q`?dvS{5s-sZsk`gJ<%1X>=fMgW(j$9U+5L- zR=J=1f9AOU%jmFOMWjKUH)TT*sV3E)H)Ls97;vjgOr|9+3fHzBiWJeitSd#ss8>7a(QiS|X!J8M2qHy$J zVJ|(eY{;2^T28!f?j(r3A`ak?d3^j#4$FAW8LwV#WbT{|Fq!|Hf%3rvY3KlU+(zx> zGMgVa>2W^4Nam;qYJ{tmqI~JqD}MD@6ON1FS>TDlcoOS%BQ>HGhBPp>JoegXi|D_< z1QW_DC@dz6JFzMN>J|Wg7*VgFbHeOf*nIS|>l=dq2zi&0A1VG>UEKq;BU8@{?_@t> z?PQj~MS-M9Ur&GzNKQ_!b*%95neTW}1baSc1ATQuu=Y#tYwPIC4Fx=AQNC_oC8Hg- z8Yuh<-~J~U%7Rs?ZfbhbtxMhkZNGmof0|2jCSE{Gd+Wm4^!Ox7VOkd8CtYQ?vNUxfcxWJb?RJ8%=sBkyoJCwKO8l zT)=x_d_>v=$m8*Fu?Hk?tVuapY#RJev~D?p6F*{pHo`LU_}Rh3uffUcVL!aQWyjbT zt5ij^_(CX$cLH$=db?r=4evV$?0I)^(J9VyeJzNV`P#CREX((n$Resac{XzYL3G~N zQq9m3neuf)8{wr2dWZIB1}r`YXWL^veDzd8!py)cT4HeyfMb4GROBE$M+;3}QMTE0 zn_n9FPG=$P4IWzQ%bD&sD_5Eab0Y);niN5Ovdy^bMOdhJC=e0)5#ZrbD7Ki_L3Vl=5@KJ%gom75qPQYZSA zCcp2kq7JX!WVX7*akvJ_t3(0WU8FF@@wGiOvaF&fzQYA1zl0Kcd(U6x^fDqtrZRO! zrHmRhvXnFghAlu9^)n~ZwQf?mH5UnmWi(jBfzk|eGIH~~Qqj7MEenoH2^FtF-x8i2kjrBl)axnlNdvn8@#pFB1kR;Snbm&LQshaJhgra_B=6!Ss z%T#D4zFR$s%%K?(`{*VrptXn!J;!>N7H~or&v&EF1&{%gq5N%T`wQ#_`Y&^5Fd!4F zXV3Ioc<~E}YvOt>ZNCr4t|+glHyRifr*d85%kWhU0U2@ZH(oeRHN8FFl69?bHpc?8 zDG~0LwSa^|f47;+i2N6h3IfG!k_f#pKE>tJ1woWJfpvh1&&br4?cp;8 zjY|j40~RWk86r1hcy0`{yRalsMJ0n3SVC4G?ysAeM!Cz{#@*%41uL6rplDsiB{vVw zSq}p_zg{V|=L>7B;|(uG$d6BU5xXl-aOe_G7jC4c&TjTDi2}MBOfH5puH#7K1Ycnd zP!wb+g%66?g9S&SMYGH_A1AJL(2ZK&c<-gv=IEq zi=T(*1=9GdufrV<#>ZR2#2H~Fi10O@LXO0mWmkDrP`f>_N&mflL;)a)3C3)Ia+3$- z0ef_&FyF`?u6?_#7su1NS7@A5Cb_mWh7D07*-phbD0#l6#}HP?XOd$dR>IIcPaaWL zvdH=ZHjX}v27|BX4;EVO^kENBTy3*{mg7$M&|RDFW?XTOEY`XiICI< z|5^#;eX4!L_&f&)Awey}YDPEhG40(KiM(n)uN)MvHfGXS_dc}bM%z5Naa09!NE1HE77KVg zW@@p798fsnZ~`V&MKakt;}P={4N2ikES5oB;9VPI@(d5gDwzS%wolniojA4FB8>`k zoNC_1MKKbgr0_;IPmRE6c&`czvc(tlXcUvY7e!^Ns z6Im`rK}`eC1ZX4PH6g7Eybe`H7aRM#UlubQ{At?W4$?{KOh@jk;JrJG4X_$IvDRA?--JTldh|3Vb5D#?6Q|ASYyb%0WAKzyK@Z%2jxwK<^7*QMPn^N5>tPnT=kZE;(%yx99#eEBH`=qVc zz6v9ZQUUU7ye01%;`UXp%y;pF9Bf69wF-2 zinvFpy&SZ>w5#*V4w82a?J9VV;o`b@xHwp#HDnZ;383A+ye89Qgams&>)sh0JuXU( zagZ^1l;uZEI;)mGPXKNe3@Y-mLk_C}#?D2Y2iRVR!QN4`UJ{5W zkeU&xQf{0yd0cD>ewz={>z{JmOc~+<<~in(fTTw7H4d;Qcxm)Nr!s)Qjm)@4aK$9S zt9(Vyzhu`HS@gA;5g>PzuPnxa=Sbl*s{T394_gbQG;TN9t(R6?)ZkwA_RE zW`uSAb+cd)qK|zhx(X0|NC*=~D?VUr(Jz)qgYJ#rom1%VZ>w^>RZcmBYQq0*ejBLK z$EsRBR)htHs}hn1a02cATt|7QaR1B+rF_y(mH4SPa{z0MY-2WZQME7cg>L2mw;$m# zRSVZjMy8$MCDG%Fc#p3q(Tv;T=emH^$TVQj=#$`uQ^XBOXs#2qZjB#g@Caf50Lz`C za2rbdz(`@^t(_JigLwZ5P$OT^Y<2qIMuZ=TAO_H_kYh}&zA9Hc+R=J);nciBHO1GrLnjs& zsn+H-R~|;~dP08)6n^VZ9VX-e218EtYg=OZG|{gzxjJAPHp~qo4)CD|cz=#_UU3^> zg$wZPg~3|NNPo!v&Ir72XtYxSg`CgER3{gQ*NUTMmKC(7urdS5{!(_xlqUZ(3oJIpyE@++t9?+d&fl)V; zLk?wN{v6v$k;#bSJ^$D<R0_Wf<;2sSFa+lD)`F04R_fm5hprk}2hV zxc&Ur^+I9X5;1bgE4Jd*GhTc7=KRC#b$p}kP36pKJPD-t;A7-mrsw+7uO-7o5*PXR2V$I0_M`DAsWIfOvo z_pUm4sixs$(~=iXQa`i%B8v=k){s^JTA~&ZT<%L3R__aY$pf_Uaq1UZiw(~AT1Qp0 zpf$)JfZ;D1O6MV1Mb!8y7;M%!4zv7`=>`z9SuBIsM+#l+Rg4@PBT{9EY4-3H(t#0- zHDY$)d&+YjV`>?9z`cUbZ~>5E?XV+lgOr{J<&=SW=XPhTn6VRlQOx|N!DRNat%=}( zs8~k1#9*$@ipIyn7h@Bl8k)!y$AkN`Fxw^}=SR+#7-Bp@{q)#hcR1Z%1JADFUQ6)i z_lSbe8dm=&vi3^B7nFo^z0Po0Q2~YoRe#k(n$bYZWY|nVrxnUk@UP^TZlR^p^6re& zhyVJMMxZP-@EWYgcW9*DjcWq8DRkkmzU&^r z)J9iV>H=V;A3UtD|c#|#V z&acS}zEjl#(QZ~cw`t|Rs7hQziW%*Gcs98L4QOjNX`Ur;?V*tT$kLiOqP05KK~N!W zr0<>=^YDo1F$ShPQh%JO0^14FG9Ks02DQ~2xc59d8=GQ07C=>o0H*SQf4El$YJj>| zodQ&h4a(8@iel=ZFOamCL}Jc;<)(AdO&5J8BZKMA3zZo~ooIhnOMgrPTWC7l+MDK| z`c(P(pH4?taylQhpqrTt-px|zY5=~Zsf(|G7Q@5;_dqsyL)zE?nyaxG^cJb2h^N8R zL8pv4<;+9?Ek!Bor(l@df;oiu8`}1ME&~5FDDA(CKcKQ5wW0{_3^sDa)?11h#&OMC zVb|LV(ZENbHR6NIOm^%$?&_K*fa)3nr4A&|D!N?=xhl_lLsasKyUL4_FvdyLC|nnH zds<@nm)aq-nd|5KcPgiq?j2M)M`p0f>xdsE6j=-kL`T(*0mrKDwZ+iAHK@RiZlpe_ zy?U>{S0C(?eFQ!uD0zN0)rqwDzwF%e^Yi<+q}{_F5FHKBkS5}{!<gq6WvD*qubL z9C#@qxymoizn<1SU3<}D9uJ4g+>GhJiGQvKC)b;Abn`WdC~2qv`n+z+z1O;8;KVcn zAnfAb+ynoE-XL_E`~65|)p;_)^rl|K9Adq33Q!Ru8mi931Ux{#C0=uK=G-7f&ix|Q znSl&B#ljS_9}zM(EpyxN=ucYnc=Vj$sf^S>nFxwMt$8A$)sh&h!AmZ-tv51a{qjh7 z=)L7Be_wV81unF12e_t^&Kx#BP!+VNJM=+%^`=Atc+$?Lakv(M7M5i9kCM`;ud6G{ z`)IGzC9RHOT*FNwq{!GLkfdluvoE#f(`>x}`*hWXka8h84pK-yXhx#V2e48$8k`lhvWKgM`uY6rnZn zo)VqyC>5)rrTsB;pRr8;=&yBMp_~w7)|h6bEU-kRL=sjUA8lUzh|s?CAuvq}#&M*7 z_9IVR^EPm{%jk97WQSqmu^P7@K#OMPOB9y*eD&6ldhHEO zf&9s#r#duR4j2KRhDs=3S5bUs(tI95wlRfl4Kkx!GgAdP{chqK54y9QS+lZfDxkWldbVl4zFpA9bq1JDEj_Nl`5+ zO_F3QE2|H#&5@bve(&QwOzrodvqz3e?z@+!j(Pa(2U$Dk5~qAkDSM6h_j&of!zOg_ zIrzgTctaZ^D)&B|>m*BDp9V=c2E{~( zgB9E-duogp_b=|>M=_6^s9e%=_kUvkCZ0g+w@mN%nQq~J;3NmHhx)1t|0P9KPsIaY z$UV_vL}?3B(-orgk_vE6o2C`?CNGjRPysJk;0JB5L% zL-X0_we)kTfL}PR@V--oia*YOvZm(!@Xl&^xf)l-WVE{`|NLp^hj*~uuh!v1qTn?B zn=H-;U_*6d>=fJBz)$#oRqp? z5bZYdj+Vj>L?6K7YbjT+Y@UEzZ^U!BGpa7^rad5I2 z%3YE@vwX^f)b!AL0hzW6cmIBznPEc&bfZCjsk=sB$=J_qN^m0~`N8kmNP||$48Azi zWGbB&8GU0`T4Y{}D<3y?e(?se|58WwbHwJ>&jM^E44GH~%zZ2*X`uead*O1q3jdzm z^o&lFE{=tLDKftorpIX9)?&D-4hoH_U)>g5QRkEUPmZd(fpD6k)+m-kcF-hr?K6e_ zACbX!V8s&yG+e^g=E>Ft$Zd5nj*$E5eN~^xCNLrC=h(~48f<-CIH=QL6mb(i&j9tcH3wI09qv^pI-0DHDD=1K~C<| zx13GVT9RSv4#wbRU9(3o{|ioDm&3<#;g1U@f8yPqq>+=f&+P5@rAQr%ORkMetT;+= z5S}-0Q@|H<{`pEMR-^bl2Do1?1w0Wg<7=N_+i;WXq~ z*AG5rGqwB?*E3!YD+C|wfE#@xg_Ko=j6ZKIxOQz{{E6LY0mzAH*?N`AG+ZBgb9vwzup7Bu&NL_rRzn_eXcdwvY>vL{+ZGE1E2q8aFZVn zk{8 z*{!>Aqd0hfD*PZ7YOp3e{u=dU&JiS~xkDpsd2kU8{4=6|6}5{y&ugxiIW*?p%;s@`cCCt5dG_l}hseI1>r zK*>!V3$P;k=iL#x*s^WWD7&OB+W(o`#Wto7WSRPXc(v=-L{kk}w|y6b0u7XUq|~N< zR`o0|Kj9G{jwV)E$f_#okB2Z5SCTga^c-iWmVAd>ja+FT?pISf73Ydh9I|$$TKmXK zNnIxJi5FSGC1&suoq`4NLp!|gL~d16CMwq!4s($^z0MwgQ)PQh$PA-_bnBy%f#Ryi z^;;ZQX%ua#t)`4H9(q}QTn{dFRJ|wjiJr_~9+uRENKaQ&gT`#s(fME44lCYy_EUaZ zOH>c@gZ_hK0zD|nZzivun>ll&2@JDVQH6TkbTA$z6Dm4WZ_R02+RUnMzmwr$>($$6 zd$J>E5=V5yU!T3uPX&X!S3DdWNyVOJGvW7N_@K=;b!O(U!!;VzfpoqDNai(nLuO-- zcEc4jVP0@g8&ZRZ=*VY)J!`{v&X}m(O;Nw3vn8qQ=VnJ_#wI!4!*pE*C#n9(rOf^n zYy)Tg^y6skkez?&b;m2QdpL7KI3kp$kVvwCV|$K`n8eyMp5q~L+mkVK?I2Sz1Hr$qUPnACNL8~_dtR4* zatUkuIx4Aw8ihe-4UlqL5a~qHm&@H%k3<;24vR)>xhbsQS|BR5!99XlbSTT@zsT$l zA>CpshRuDYDkZO1P50{yD-R!hm{KY%Qc-@Y09chyU3J_?wjnFetf`_V*f$VeLdnZN zAAk01xBM36<6Ws@95--AoF^}S4zhXQ>i}A%ot=Co zvvDLSWXi40W zuj|Fc(L770L;v|;Q~ws0VIdK30J>o%L@P(eqj zKE?WU!m5v8@0P&I*-eSGLk+*!+NEa5P(%Ok<>*CBc*3gLb5$Kcl3XximJ!WE&U|MUcX=Q)$Wan>C34$F~H4Ha%7WrLhC`C=8;|f?Dm`QYoZH`s|so* zS8lqSKyIQm;$yeXk``knoYkej+@pQf*@Sv5g|Q`ycK+bfqs6LA?D6B9hOb|A)s|X$ zVU*m**Dhg|IwWL{bg;zq0PC1Gm^Ls8Ro9Z@+V29IySiSM+~^UIlz;N%wko$+&rP?D zq^?FbTt8eX#Qy-;HFvG-`Pc|^=a?3HVzKOB(i<>4JL~GR)Y%qOI_NGjpibR`Tw?5w z=02`rF#^gs-FpPcCKvhN$Y#XxCL@NgNdRX@x1VRf_?4=^MwDZvSD5i00ruy5s zMc_Z18aR+_*+$@^OD1GxR9joyHMFaUanP^MjOLE|0|Wv&)FVB;E2Gqr?nP8pYapMm zXMCug%FVma93DASUa_q!|CIcCJ@nbLX9@$YxYdJH27K>ZA`P^EuQH?f2AZTe_p;=y z`jy{eIS7zI1gOd*_?pLoB4kmZK^f*?C(i9~OK7V{p-0A(`YatJhMeO%>U6)L(S1MW zQW(_n?zc`)PZ1fXlz$#u{*G{V-rT%9>^M4{!UZ&y{pw*z11bPpqIJ}ve@Vw~v%i2V zkJ0R9ID)($E&TSJHtr8y!TU6@t!(h=25%ZzZ@&29kE)%K%gR}?S$nlr!&Hqb%Avm6 ztzsncHe<#jy+Vi%KwS}2&;r>6m303`1bPqR2;2hUWpy_yfHszY7s5$Vt>cJg;*f<| zGjsEv&aN)BRCtXJk(mFZMxO4N$$R2BPKpO&S>^;Kf&PG~_`Lxg0QZ%W2MG4qRz}>J z;YXUA1oJW~C)nU!R)KwOi88fJyw-}p2}fXs#)>4X%V6Y8rP=Rr&7X;FMUPe-r~oTy zItLV)84%YlRp9;;^z73m6Ebki)e)0_{f(a;v+i_F>x7q7v9*N-W%#}arEnY*%ws~1 zV#L$aGe`9SXQQ_i$9Fr%=&ie5Lu!%y2hy^Q+3QOz(P^P_atE_JFS}zOt-IeES=ZyA zq-Z{_OmP@)TOUkCunzh?QU#f-sX%SryCnl z=m$?K_nrE;VD@Czw`2NBKHZ-)XM3V-uZBHZZta}z7n~jiCoEXf4Rej+nW9)|XlS6C z8z7%8j=%`o_S7YlM0H}t^Oxm$X*KsEf@s4n-ygqzMX*mXht$dMhY(RSa^7o%2`2EV zf_%x#0s~73NV*u4LZ}!-upElb6d+w^IbEzVi6om8}GXiDgYeE+}tp(_)*U2;LkHU*MZV-ERRgAfv$oT z%lp`qoCw*JZyOZ@lO8P?@RZOK8-X&C6Rma>{kaBcJ6+-)HygjIppHHwRmAUqGr*fI z{h;2==30!$$l@5Y?prMkxP1fBb~~tz8lFe&2q1@46d7wtkOLsz+p#B1T0b z1lum+av%?*iDSWQRbJlS=pQ_Q>IJ*pn5}jG^egF1J|*r9T12p}Z&DUEHsPv#z^9@QHSK*U zS(jtC98C!!Ym0q?4lWte-hO;ZW_)@F0eVkJs{XZU?1@OU{k}az$uKiJE9@Fl^qtt`7nI=0g~O%r zKOSJxzwMV7V;1-gAQ}5{Wmi4yFcTmNt?#6VeP=)nLpV})VVpiTR&8DsEB0eOPj3sH zeh}6BGRh}h$*>FdcR+w|cg~*B^7(ps-8+Sa$u=sD{~b|2!K#&_S9CAN>^{PGj6x28 z8H3t5+?JhqvRx`({jx7jvZb0}$ajBh>Ic&Yjf6op52`wYlpX;^Uu0zAm5?d2f<%C< z?I4Yp%xa16LP{>q?QYh^HdL<(6Xep0;XN$gi zZQ*@uN8-E=IqxX=x}?YtD7;4e>F<)L5_k9qg2GtU?b8j+y1xA|fOifycBW`m;zkn= z9Q{k`p{YxXHm+_JAVE|^kk18ocY*z#9-F_b}=W>OI{ux`NFYnnmmKb9mP$AAyJrDEFEAQkm7Y;PXrh09L~q^cRg8}y*7l* zjuUn#3H=70?GWiUT&d< z`+Irqi-z9%@~+@vS(!1oX1bLvOLbz3$Pn=c@IZx0%{D&TDzh4fZ+)z@uShi2o`0Si z&Pm!>|Mc#;;DaT+ArPC_@{Rds`Rg^O9?rqm)5L%F@gE2yJ%sfb|OH{Y+0rs1M9 zi2nitE}Pu$&o+o8fz87&maH=*BsYY^0OyIzQq5Rckoi=eZ-WhTa~I!kH`IQ+D3-(+{4B zBqy0s;iM(nCe1+U2R=nq7%{D?rxUF-5TeBU0JCSEVL~|tOFPPpdl7VGyA%e8wx#h) zg==p*F=L+h;iUCG!FGzW(^ho>KJk>BFXA5U02jY-POKuCvZn;7$K$U<*fdw{PUH`& z3{(n)SC}Jo0P&RAg=sexC@OA_rTBvB?H7ieoE!v=<6;j%OO-3znyTEpz=^$uxST9X ziLRp&-0t?{^Dko+8_JZja?;XQwBK5;q8HfgAVkfi)L-*T!QY}vUzunvyR|Ni9hDob zkb5sSF0XK}EGdTGl|;s2Adezrh~ewEPrMhVJ+E2f2A5xj9N8e*h&K|yhH-)^2Cu&1 z3Oh;}_R9W~`2}O1(P^>jn&+e65W{ImLVCuQAk@ZCP1aSPo@RrOrro--1}QL>CvrETw_043z8smdgXqq$@VtcwISK@ZVk6RQuF=4le0LbiR7n8 zLrdXz?CXc>z-c)%=WLnq+w=5JQCz9f-zPnVx6wiAy5ED`bfd)=^i*wJkB>+w{5sp8 z8K$RDv*CMo3Od&QMKkj!YsTkqJt6LjZBtnebYm5xh}^h!=bjxrMw86e+aU%-FnArs z1lNVV4)mh9Y3n;2;@XXqZyb+}88R(Os&INsfNq$~ZE4)(_#?C+I-uSXL}z$#B_lpy zDskwX67hj1o^4%&FY_w@1d0z@p|lU&sdw~ba}5-DxDf4>o}_$cwAOwN$5*?r+upMA z{NWN>8_jAy)?VoyU8ofOmXq`Q*zKE;5e5;gzG%>kookFvXO84^C zcP&0I3rP>y1jjKJM2agpsF7y{k4nMS6$WUk>ZyEEvXA;d 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 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 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 87a7f340e3100b7f1ee1f9a2c4b05abdb194b86b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Dec 2019 18:12:44 +0900 Subject: [PATCH 37/96] 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 38/96] 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 e05c9426ed56921fc982c10e9cf5d3be169ebc78 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 13 Dec 2019 18:50:49 +0100 Subject: [PATCH 39/96] 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 40/96] 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 41/96] 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 42/96] 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 19a3c959923328c8b91e4653ee003b101f60c770 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 14 Dec 2019 21:27:26 +0800 Subject: [PATCH 43/96] Update InspectCode to 2019.3 --- build/InspectCode.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/InspectCode.cake b/build/InspectCode.cake index bd3fdf5f93..06c56dce87 100644 --- a/build/InspectCode.cake +++ b/build/InspectCode.cake @@ -1,5 +1,5 @@ #addin "nuget:?package=CodeFileSanity&version=0.0.33" -#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.2.1" +#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.0" #tool "nuget:?package=NVika.MSBuild&version=1.0.1" var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First(); From ab70abe8bd2b577c049cad84283f0b74e1dcbc60 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 14 Dec 2019 21:28:13 +0800 Subject: [PATCH 44/96] Turn off unexpected new warnings. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++++ osu.sln.DotSettings | 1 + 2 files changed, 5 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ec524043ee..4acc619753 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -587,13 +587,16 @@ namespace osu.Game.Screens.Select switch (d) { case DrawableCarouselBeatmapSet set: + { lastSet = set; set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); set.MoveToY(currentY, 750, Easing.OutExpo); break; + } case DrawableCarouselBeatmap beatmap: + { if (beatmap.Item.State.Value == CarouselItemState.Selected) scrollTarget = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2; @@ -619,6 +622,7 @@ namespace osu.Game.Screens.Select } break; + } } } diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 9b400de390..105d22fe3e 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -215,6 +215,7 @@ HINT HINT HINT + HINT DO_NOT_SHOW WARNING WARNING From 8b570233495c2c654b9493bb7d6415580c4c8174 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 14 Dec 2019 20:22:39 +0800 Subject: [PATCH 45/96] Require 3.1.100 SDK in global.json --- global.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/global.json b/global.json index 43bb34912a..6858d4044d 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,9 @@ { + "sdk": { + "allowPrerelease": false, + "rollForward": "minor", + "version": "3.1.100" + }, "msbuild-sdks": { "Microsoft.Build.Traversal": "2.0.24" } From cea3a66d4a0f9e235aa7d5e3966281507229cfc1 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 14 Dec 2019 20:26:28 +0800 Subject: [PATCH 46/96] Use static local method fixed for roslyn 3.4 --- .../Visual/Online/TestSceneBeatmapSetOverlayDetails.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs index 2a45e68c0a..96c0c59695 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("set second set", () => details.BeatmapSet = secondSet); AddAssert("ratings set", () => details.Ratings.Metrics == secondSet.Metrics); - BeatmapSetInfo createSet() => new BeatmapSetInfo + static BeatmapSetInfo createSet() => new BeatmapSetInfo { Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).Select(_ => RNG.Next(10)).ToArray() }, Beatmaps = new List From c457571da6d69f41ac7722bda6532fa99722c611 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 14 Dec 2019 20:54:22 +0800 Subject: [PATCH 47/96] Use index and range expressions --- .editorconfig | 4 ++-- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 6 +++--- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 8 ++++---- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 4 ++-- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 4 ++-- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 +- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 2 +- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 4 ++-- osu.Game/Rulesets/Objects/SliderPath.cs | 6 +++--- osu.sln.DotSettings | 2 +- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.editorconfig b/.editorconfig index b5333ad8e7..8cdb92d11c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -176,8 +176,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = false:none -csharp_style_prefer_range_operator = false:none +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 9069c09ae4..6e4491de94 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps prevNoteTimes.RemoveAt(0); prevNoteTimes.Add(newNoteTime); - density = (prevNoteTimes[prevNoteTimes.Count - 1] - prevNoteTimes[0]) / prevNoteTimes.Count; + density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; } private double lastTime; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 5f75cbabec..b6fc9821a4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -286,11 +286,11 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great; - private bool assertHeadMissTailTracked() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; + private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; - private bool assertMidSliderJudgements() => judgementResults[judgementResults.Count - 2].Type == HitResult.Great; + private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great; - private bool assertMidSliderJudgementFail() => judgementResults[judgementResults.Count - 2].Type == HitResult.Miss; + private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss; private ScoreAccessibleReplayPlayer currentPlayer; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 9b820261ab..2497e428fc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override bool OnDoubleClick(DoubleClickEvent e) { // Todo: This should all not occur on double click, but rather if the previous control point is hovered. - segmentStart = HitObject.Path.ControlPoints[HitObject.Path.ControlPoints.Count - 1]; + segmentStart = HitObject.Path.ControlPoints[^1]; segmentStart.Type.Value = PathType.Linear; currentSegmentLength = 1; diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index bd59e8a03f..2686ba4fd2 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -156,9 +156,9 @@ namespace osu.Game.Rulesets.Osu.Replays // TODO: Shouldn't the spinner always spin in the same direction? if (h is Spinner) { - calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[Frames.Count - 1]).Position, out startPosition, out spinnerDirection); + calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); - Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[Frames.Count - 1]).Position; + Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position; if (spinCentreOffset.Length > SPIN_RADIUS) { @@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Replays private void moveToHitObject(OsuHitObject h, Vector2 targetPos, Easing easing) { - OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[Frames.Count - 1]; + OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1]; // Wait until Auto could "see and react" to the next note. double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); @@ -363,7 +363,7 @@ namespace osu.Game.Rulesets.Osu.Replays } // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! - if (Frames[Frames.Count - 1].Time <= endFrame.Time) + if (Frames[^1].Time <= endFrame.Time) AddFrameToReplay(endFrame); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index ed44d82bce..b0b673d6a4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.UserInterface private TimingControlPoint getNextTimingPoint(TimingControlPoint current) { - if (timingPoints[timingPoints.Count - 1] == current) + if (timingPoints[^1] == current) return current; int index = timingPoints.IndexOf(current); // -1 means that this is a "default beat" @@ -169,7 +169,7 @@ namespace osu.Game.Tests.Visual.UserInterface { if (timingPoints.Count == 0) return 0; - if (timingPoints[timingPoints.Count - 1] == current) + if (timingPoints[^1] == current) return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index ce2783004c..03496952e7 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -195,8 +195,8 @@ namespace osu.Game.Beatmaps.ControlPoints if (time < list[0].Time) return null; - if (time >= list[list.Count - 1].Time) - return list[list.Count - 1]; + if (time >= list[^1].Time) + return list[^1]; int l = 0; int r = list.Count - 2; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 2b914669cb..e401e3fb97 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps.Formats if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal)) { - if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) + if (!Enum.TryParse(line[1..^1], out section)) { Logger.Log($"Unknown section \"{line}\" in \"{output}\""); section = Section.None; diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 2bbac92f7f..9735f6373d 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.Containers foreach (var link in links) { - AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd)); + AddText(text[previousLinkEnd..link.Index]); AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url); previousLinkEnd = link.Index + link.Length; } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b5b1e26486..7fddb442d1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Objects.Legacy result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples); // The samples are played when the slider ends, which is the last node - result.Samples = nodeSamples[nodeSamples.Count - 1]; + result.Samples = nodeSamples[^1]; } else if (type.HasFlag(ConvertHitObjectType.Spinner)) { @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Objects.Legacy { if (vertices[i] == vertices[i - 1]) { - points[points.Count - 1].Type.Value = type; + points[^1].Type.Value = type; continue; } diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 86deba3b93..293138097f 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Objects get { ensureValid(); - return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; + return cumulativeLength.Count == 0 ? 0 : cumulativeLength[^1]; } } @@ -251,7 +251,7 @@ namespace osu.Game.Rulesets.Objects if (calculatedLength > expectedDistance) { // The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments - while (cumulativeLength.Count > 0 && cumulativeLength[cumulativeLength.Count - 1] >= expectedDistance) + while (cumulativeLength.Count > 0 && cumulativeLength[^1] >= expectedDistance) { cumulativeLength.RemoveAt(cumulativeLength.Count - 1); calculatedPath.RemoveAt(pathEndIndex--); @@ -269,7 +269,7 @@ namespace osu.Game.Rulesets.Objects // The direction of the segment to shorten or lengthen Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized(); - calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[cumulativeLength.Count - 1]); + calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[^1]); cumulativeLength.Add(expectedDistance); } } diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 105d22fe3e..12571be31d 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -220,7 +220,7 @@ WARNING WARNING WARNING - DO_NOT_SHOW + WARNING WARNING True From add04e98e18d734e84fd88a1aad35fe71a5c90ba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 18:10:44 +0900 Subject: [PATCH 48/96] Fix cursortrail texture not being bound --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 4d6db83d7a..4e86662ec6 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -227,6 +227,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader.Bind(); shader.GetUniform("g_FadeClock").UpdateValue(ref time); + texture.TextureGL.Bind(); + RectangleF textureRect = texture.GetTextureRect(); foreach (var part in parts) From 83f77d9c3510cd4c68f1574c2a90993d04932779 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 18:41:02 +0900 Subject: [PATCH 49/96] Make the layout faster --- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index dc32fc7cd5..840c1d37e3 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play { public class HUDOverlay : Container { - private const int fade_duration = 400; + private const float fade_duration = 400; private const Easing fade_easing = Easing.Out; public readonly KeyCounterDisplay KeyCounter; @@ -103,8 +103,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), AutoSizeAxes = Axes.Both, - AutoSizeDuration = fade_duration, - AutoSizeEasing = fade_easing, + LayoutDuration = fade_duration / 2, + LayoutEasing = fade_easing, Direction = FillDirection.Vertical, Children = new Drawable[] { From a276643a4bcc8a2a6d376ec43fee0ce816cddea2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Dec 2019 18:41:14 +0900 Subject: [PATCH 50/96] Reorder health display and score elements --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 840c1d37e3..e2f362780d 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -78,6 +78,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + HealthDisplay = CreateHealthDisplay(), topScoreContainer = new Container { Anchor = Anchor.TopCentre, @@ -90,7 +91,6 @@ namespace osu.Game.Screens.Play ComboCounter = CreateComboCounter(), }, }, - HealthDisplay = CreateHealthDisplay(), Progress = CreateProgress(), ModDisplay = CreateModsContainer(), HitErrorDisplay = CreateHitErrorDisplayOverlay(), From a85653ebec2607ca2675a1b106ebcd814c75fa66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2019 12:24:59 +0900 Subject: [PATCH 51/96] 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 52/96] 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 53/96] 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 54/96] 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 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 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 55/96] 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 56/96] 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 57/96] 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 58/96] 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 59/96] 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 60/96] 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 61/96] 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 62/96] 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 63/96] 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 64/96] 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 65/96] 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 66/96] 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 67/96] 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 68/96] 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 69/96] 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 70/96] 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 71/96] 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 72/96] 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 73/96] 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 74/96] 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 75/96] 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 76/96] 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 77/96] 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 78/96] 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 79/96] 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 80/96] 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 0a278ef9432d06ab6ae6f94be332296c5eba9e6f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 14 Dec 2019 11:40:59 +0100 Subject: [PATCH 81/96] 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 82/96] 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 83/96] 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 84/96] 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 85/96] 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 86/96] 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 0f5ef78b69c552545e5cc7dfc9b6a91bd2afafb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 12:39:50 +0900 Subject: [PATCH 87/96] 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 88/96] 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 89/96] 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 90/96] 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 91/96] 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 92/96] 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 93/96] 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 94/96] 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 826b271371188a64d9e9e6660fbe2d23af254ca6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Dec 2019 15:39:36 +0900 Subject: [PATCH 95/96] 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 96/96] 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)