From ac91f0e2707bf57eacacf079c4de09b1c108926d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 00:25:46 +0900 Subject: [PATCH 001/102] Add extended limits to difficulty adjustment mod --- .../Mods/CatchModDifficultyAdjust.cs | 12 ++- .../Mods/OsuModDifficultyAdjust.cs | 12 ++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 79 ++++++++++++++++++- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index acdd0a420c..859dfb7647 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods public class CatchModDifficultyAdjust : ModDifficultyAdjust { [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] - public BindableNumber CircleSize { get; } = new BindableFloat + public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 1, @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods }; [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] - public BindableNumber ApproachRate { get; } = new BindableFloat + public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 1, @@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + protected override void ApplyLimits(bool extended) + { + base.ApplyLimits(extended); + + CircleSize.MaxValue = extended ? 11 : 10; + ApproachRate.MaxValue = extended ? 11 : 10; + } + public override string SettingDescription { get diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index ff995e38ce..a6ad2e75f1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModDifficultyAdjust : ModDifficultyAdjust { [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] - public BindableNumber CircleSize { get; } = new BindableFloat + public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods }; [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] - public BindableNumber ApproachRate { get; } = new BindableFloat + public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; + protected override void ApplyLimits(bool extended) + { + base.ApplyLimits(extended); + + CircleSize.MaxValue = extended ? 11 : 10; + ApproachRate.MaxValue = extended ? 11 : 10; + } + public override string SettingDescription { get diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 165644edbe..7df663ad3a 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods protected const int LAST_SETTING_ORDER = 2; [SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)] - public BindableNumber DrainRate { get; } = new BindableFloat + public BindableNumber DrainRate { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mods }; [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] - public BindableNumber OverallDifficulty { get; } = new BindableFloat + public BindableNumber OverallDifficulty { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -53,6 +53,24 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; + [SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")] + public BindableBool ExtendedLimits { get; } = new BindableBool(); + + protected ModDifficultyAdjust() + { + ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue)); + } + + /// + /// Changes the difficulty adjustment limits. Occurs when the value of is changed. + /// + /// Whether limits should extend beyond sane ranges. + protected virtual void ApplyLimits(bool extended) + { + DrainRate.MaxValue = extended ? 11 : 10; + OverallDifficulty.MaxValue = extended ? 11 : 10; + } + public override string SettingDescription { get @@ -123,5 +141,62 @@ namespace osu.Game.Rulesets.Mods difficulty.DrainRate = DrainRate.Value; difficulty.OverallDifficulty = OverallDifficulty.Value; } + + /// + /// A that extends its min/max values to support any assigned value. + /// + protected class BindableDoubleWithLimitExtension : BindableDouble + { + public override double Value + { + get => base.Value; + set + { + if (value < MinValue) + MinValue = value; + if (value > MaxValue) + MaxValue = value; + base.Value = value; + } + } + } + + /// + /// A that extends its min/max values to support any assigned value. + /// + protected class BindableFloatWithLimitExtension : BindableFloat + { + public override float Value + { + get => base.Value; + set + { + if (value < MinValue) + MinValue = value; + if (value > MaxValue) + MaxValue = value; + base.Value = value; + } + } + } + + /// + /// A that extends its min/max values to support any assigned value. + /// + protected class BindableIntWithLimitExtension : BindableInt + { + public override int Value + { + get => base.Value; + set + { + if (value < MinValue) + MinValue = value; + if (value > MaxValue) + MaxValue = value; + base.Value = value; + } + } + } } } From 47a93d8614eff32b5fd7bb9db8b81e8d97a47f34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 00:26:35 +0900 Subject: [PATCH 002/102] Adjust osu! hitobject fade-ins to support AR>10 --- .../Objects/Drawables/Connections/FollowPointConnection.cs | 5 ++++- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 6e7b1050cb..40154ca84c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -110,8 +110,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double startTime = start.GetEndTime(); double duration = end.StartTime - startTime; + // For now, adjust the pre-empt for approach rates > 10. + double preempt = PREEMPT * Math.Min(1, start.TimePreempt / 450); + fadeOutTime = startTime + fraction * duration; - fadeInTime = fadeOutTime - PREEMPT; + fadeInTime = fadeOutTime - preempt; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 15af141c99..6d28a576a4 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.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.Bindables; using osu.Game.Beatmaps; @@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); - TimeFadeIn = 400; // as per osu-stable + TimeFadeIn = 400 * Math.Min(1, TimePreempt / 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } From 9835245ea29e3dcc053feb818b65f2e959cb5d06 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 00:32:31 +0900 Subject: [PATCH 003/102] Add test --- .../Online/TestAPIModSerialization.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs index 5948582d77..84862ebb07 100644 --- a/osu.Game.Tests/Online/TestAPIModSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs @@ -68,12 +68,29 @@ namespace osu.Game.Tests.Online Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); } + [Test] + public void TestDeserialiseDifficultyAdjustModWithExtendedLimits() + { + var apiMod = new APIMod(new TestModDifficultyAdjust + { + OverallDifficulty = { Value = 11 }, + ExtendedLimits = { Value = true } + }); + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset()); + + Assert.That(converted.ExtendedLimits.Value, Is.True); + Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] { new TestMod(), new TestModTimeRamp(), + new TestModDifficultyAdjust() }; public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); @@ -135,5 +152,9 @@ namespace osu.Game.Tests.Online Value = true }; } + + private class TestModDifficultyAdjust : ModDifficultyAdjust + { + } } } From 9a5790cd31dbd9a60932fb16790fea3390db7ca8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 19:18:16 +0100 Subject: [PATCH 004/102] Implement StableStorage class. --- osu.Desktop/OsuGameDesktop.cs | 3 +- osu.Game/IO/StableStorage.cs | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 osu.Game/IO/StableStorage.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d1515acafa..0dc659b120 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -18,6 +18,7 @@ using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Desktop.Windows; +using osu.Game.IO; namespace osu.Desktop { @@ -40,7 +41,7 @@ namespace osu.Desktop { string stablePath = getStableInstallPath(); if (!string.IsNullOrEmpty(stablePath)) - return new DesktopStorage(stablePath, desktopHost); + return new StableStorage(stablePath, desktopHost); } } catch (Exception) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs new file mode 100644 index 0000000000..a8665b5267 --- /dev/null +++ b/osu.Game/IO/StableStorage.cs @@ -0,0 +1,64 @@ +// 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.IO; +using System.Linq; +using osu.Framework.Platform; + +namespace osu.Game.IO +{ + /// + /// A storage pointing to an osu-stable installation. + /// Provides methods for handling installations with a custom Song folder location. + /// + public class StableStorage : DesktopStorage + { + private const string stable_songs_path = "Songs"; + + private readonly DesktopGameHost host; + private string songs_path; + + public StableStorage(string path, DesktopGameHost host) + : base(path, host) + { + this.host = host; + songs_path = locateSongsDirectory(); + } + + /// + /// Returns a pointing to the osu-stable Songs directory. + /// + public Storage GetSongStorage() + { + if (songs_path.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) + return GetStorageForDirectory(stable_songs_path); + else + return new DesktopStorage(songs_path, host); + } + + private string locateSongsDirectory() + { + var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); + var textReader = new StreamReader(configFile); + + var songs_directory_path = stable_songs_path; + + while (!textReader.EndOfStream) + { + var line = textReader.ReadLine(); + + if (line?.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase) == true) + { + var directory = line.Split('=')[1].TrimStart(); + if (Path.IsPathFullyQualified(directory) && !directory.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) + songs_directory_path = directory; + + break; + } + } + + return songs_directory_path; + } + } +} From d71ac834280c0873422a974dbc78bf73baa5ce71 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 19:46:10 +0100 Subject: [PATCH 005/102] Use StableStorage in ArchiveModelManager. --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- osu.Game/OsuGame.cs | 3 ++- osu.Game/Scoring/ScoreManager.cs | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 0dc659b120..5909b82c8f 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -33,7 +33,7 @@ namespace osu.Desktop noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false; } - public override Storage GetStorageForStableInstall() + public override StableStorage GetStorageForStableInstall() { try { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9f69ad035f..7d22c51d0f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -625,7 +625,7 @@ namespace osu.Game.Database /// /// Set a storage with access to an osu-stable install for import purposes. /// - public Func GetStableStorage { private get; set; } + public Func GetStableStorage { private get; set; } /// /// Denotes whether an osu-stable installation is present to perform automated imports from. @@ -640,7 +640,7 @@ namespace osu.Game.Database /// /// Select paths to import from stable. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(Storage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath); + protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath); /// /// Whether this specified path should be removed after successful import. diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5acd6bc73d..399bdda491 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -52,6 +52,7 @@ using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; using osu.Game.Database; +using osu.Game.IO; namespace osu.Game { @@ -88,7 +89,7 @@ namespace osu.Game protected SentryLogger SentryLogger; - public virtual Storage GetStorageForStableInstall() => null; + public virtual StableStorage GetStorageForStableInstall() => null; public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index cf1d123c06..11f31f7d59 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -16,6 +16,7 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -71,7 +72,7 @@ namespace osu.Game.Scoring } } - protected override IEnumerable GetStableImportPaths(Storage stableStorage) + protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) => 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); From f0fdad2f838ec4ab18a82841f4483cba66f715f0 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 22:04:46 +0100 Subject: [PATCH 006/102] Construct a DesktopStorage pointing to the absolute path of the song directory. --- osu.Game/IO/StableStorage.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index a8665b5267..c7ca37a163 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -29,20 +29,14 @@ namespace osu.Game.IO /// /// Returns a pointing to the osu-stable Songs directory. /// - public Storage GetSongStorage() - { - if (songs_path.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) - return GetStorageForDirectory(stable_songs_path); - else - return new DesktopStorage(songs_path, host); - } + public Storage GetSongStorage() => new DesktopStorage(songs_path, host); private string locateSongsDirectory() { var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); var textReader = new StreamReader(configFile); - var songs_directory_path = stable_songs_path; + var songs_directory_path = Path.Combine(BasePath, stable_songs_path); while (!textReader.EndOfStream) { @@ -51,7 +45,7 @@ namespace osu.Game.IO if (line?.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase) == true) { var directory = line.Split('=')[1].TrimStart(); - if (Path.IsPathFullyQualified(directory) && !directory.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) + if (Path.IsPathFullyQualified(directory)) songs_directory_path = directory; break; From 51d4da565c87192549f62fa94cf624818809a3d8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 22:25:49 +0100 Subject: [PATCH 007/102] Fix ArchiveModelManagers lookup paths. --- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++++++- osu.Game/Database/ArchiveModelManager.cs | 12 +++++++++--- osu.Game/Scoring/ScoreManager.cs | 3 ++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 42418e532b..a455f676b3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -64,7 +64,13 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override string ImportFromStablePath => "Songs"; + protected override bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); + + protected override IEnumerable GetStableImportPaths(StableStorage stableStoage) + { + var songStorage = stableStoage.GetSongStorage(); + return songStorage.GetDirectories(".").Select(path => songStorage.GetFullPath(path)); + } private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 7d22c51d0f..516f70c700 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -637,10 +637,16 @@ namespace osu.Game.Database /// protected virtual string ImportFromStablePath => null; + /// + /// Checks for the existence of an osu-stable directory. + /// + protected virtual bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); + /// /// Select paths to import from stable. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath); + protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath) + .Select(path => stableStoage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. @@ -662,14 +668,14 @@ namespace osu.Game.Database return Task.CompletedTask; } - if (!stable.ExistsDirectory(ImportFromStablePath)) + if (!CheckStableDirectoryExists(stable)) { // This handles situations like when the user does not have a Skins folder Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } - return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray())); + return Task.Run(async () => await Import(GetStableImportPaths(stable).ToArray())); } #endregion diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 11f31f7d59..6aa0a30a75 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -73,7 +73,8 @@ namespace osu.Game.Scoring } protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) - => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)); + => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => stableStorage.GetFullPath(path)); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); From a4a7f0c5787a1fd0a5470a8af72c1ac5211e7d1b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 25 Jan 2021 19:05:16 +0100 Subject: [PATCH 008/102] Address CI inspections. --- osu.Game/IO/StableStorage.cs | 12 ++++++------ osu.Game/OsuGame.cs | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index c7ca37a163..85af92621b 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -17,26 +17,26 @@ namespace osu.Game.IO private const string stable_songs_path = "Songs"; private readonly DesktopGameHost host; - private string songs_path; + private readonly string songsPath; public StableStorage(string path, DesktopGameHost host) : base(path, host) { this.host = host; - songs_path = locateSongsDirectory(); + songsPath = locateSongsDirectory(); } /// /// Returns a pointing to the osu-stable Songs directory. /// - public Storage GetSongStorage() => new DesktopStorage(songs_path, host); + public Storage GetSongStorage() => new DesktopStorage(songsPath, host); private string locateSongsDirectory() { var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); var textReader = new StreamReader(configFile); - var songs_directory_path = Path.Combine(BasePath, stable_songs_path); + var songsDirectoryPath = Path.Combine(BasePath, stable_songs_path); while (!textReader.EndOfStream) { @@ -46,13 +46,13 @@ namespace osu.Game.IO { var directory = line.Split('=')[1].TrimStart(); if (Path.IsPathFullyQualified(directory)) - songs_directory_path = directory; + songsDirectoryPath = directory; break; } } - return songs_directory_path; + return songsDirectoryPath; } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 399bdda491..78c4d4ccad 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -28,7 +28,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Collections; From 9f9206726a0cd9cbae347ed26264434f37a86738 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 18:11:54 +0100 Subject: [PATCH 009/102] Fix typos. --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a455f676b3..43b2486ac5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -66,9 +66,9 @@ namespace osu.Game.Beatmaps protected override bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); - protected override IEnumerable GetStableImportPaths(StableStorage stableStoage) + protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) { - var songStorage = stableStoage.GetSongStorage(); + var songStorage = stableStorage.GetSongStorage(); return songStorage.GetDirectories(".").Select(path => songStorage.GetFullPath(path)); } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 516f70c700..99301b6c68 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -645,8 +645,8 @@ namespace osu.Game.Database /// /// Select paths to import from stable. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath) - .Select(path => stableStoage.GetFullPath(path)); + protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) + .Select(path => stableStorage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. From 043385f91928204cba2a292469eecfd65fd086a9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 18:26:01 +0100 Subject: [PATCH 010/102] Rename const and fix unintended tabbing. --- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/IO/StableStorage.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 99301b6c68..ae1608d801 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -646,7 +646,7 @@ namespace osu.Game.Database /// Select paths to import from stable. Default implementation iterates all directories in . /// protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) - .Select(path => stableStorage.GetFullPath(path)); + .Select(path => stableStorage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index 85af92621b..88a087087e 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -14,7 +14,7 @@ namespace osu.Game.IO /// public class StableStorage : DesktopStorage { - private const string stable_songs_path = "Songs"; + private const string stable_default_songs_path = "Songs"; private readonly DesktopGameHost host; private readonly string songsPath; @@ -36,7 +36,7 @@ namespace osu.Game.IO var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); var textReader = new StreamReader(configFile); - var songsDirectoryPath = Path.Combine(BasePath, stable_songs_path); + var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); while (!textReader.EndOfStream) { From 2a2b6f347e7e8af533e3a9053497fe7ee77898eb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 19:07:05 +0100 Subject: [PATCH 011/102] Use a lazy for delegating Songs directory locating until it is actually used. --- osu.Game/IO/StableStorage.cs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index 88a087087e..ebceba6ce0 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -17,38 +17,43 @@ namespace osu.Game.IO private const string stable_default_songs_path = "Songs"; private readonly DesktopGameHost host; - private readonly string songsPath; + private readonly Lazy songsPath; public StableStorage(string path, DesktopGameHost host) : base(path, host) { this.host = host; - songsPath = locateSongsDirectory(); + songsPath = new Lazy(locateSongsDirectory); } /// /// Returns a pointing to the osu-stable Songs directory. /// - public Storage GetSongStorage() => new DesktopStorage(songsPath, host); + public Storage GetSongStorage() => new DesktopStorage(songsPath.Value, host); private string locateSongsDirectory() { - var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); - var textReader = new StreamReader(configFile); - var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - while (!textReader.EndOfStream) + var configFile = GetFiles(".", "osu!.*.cfg").FirstOrDefault(); + + if (configFile == null) + return songsDirectoryPath; + + using (var textReader = new StreamReader(GetStream(configFile))) { - var line = textReader.ReadLine(); + string line; - if (line?.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase) == true) + while ((line = textReader.ReadLine()) != null) { - var directory = line.Split('=')[1].TrimStart(); - if (Path.IsPathFullyQualified(directory)) - songsDirectoryPath = directory; + if (line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) + { + var directory = line.Split('=')[1].TrimStart(); + if (Path.IsPathFullyQualified(directory)) + songsDirectoryPath = directory; - break; + break; + } } } From 383c40b99268b350e35d955b638781c75e09e682 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 20:35:42 +0100 Subject: [PATCH 012/102] Address remaining reviews suggestions. --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 6 +++--- osu.Game/IO/StableStorage.cs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 43b2486ac5..4825569ee4 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); + protected override bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ae1608d801..fd94660a4b 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -640,10 +640,10 @@ namespace osu.Game.Database /// /// Checks for the existence of an osu-stable directory. /// - protected virtual bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); + protected virtual bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); /// - /// Select paths to import from stable. Default implementation iterates all directories in . + /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . /// protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) .Select(path => stableStorage.GetFullPath(path)); @@ -668,7 +668,7 @@ namespace osu.Game.Database return Task.CompletedTask; } - if (!CheckStableDirectoryExists(stable)) + if (!StableDirectoryExists(stable)) { // This handles situations like when the user does not have a Skins folder Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index ebceba6ce0..f86b18c724 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -35,12 +35,13 @@ namespace osu.Game.IO { var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - var configFile = GetFiles(".", "osu!.*.cfg").FirstOrDefault(); + var configFile = GetFiles(".", "osu!.*.cfg").SingleOrDefault(); if (configFile == null) return songsDirectoryPath; - using (var textReader = new StreamReader(GetStream(configFile))) + using (var stream = GetStream(configFile)) + using (var textReader = new StreamReader(stream)) { string line; From 623b47f9af503a400facfea44e2db230db42a279 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 21:25:05 +0900 Subject: [PATCH 013/102] Add flag to toggle follow circle tracking for slider heads --- .../Objects/Drawables/DrawableSliderHead.cs | 14 ++++++++++---- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index acc95ab036..c051a9918d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject; + [CanBeNull] public Slider Slider => DrawableSlider?.HitObject; @@ -59,12 +61,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); Debug.Assert(Slider != null); + Debug.Assert(HitObject != null); - double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); + if (HitObject.TrackFollowCircle) + { + double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); - //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. - if (!IsHit) - Position = Slider.CurvePositionAt(completionProgress); + //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. + if (!IsHit) + Position = Slider.CurvePositionAt(completionProgress); + } } public Action OnShake; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index f6d46aeef5..5fc480883a 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -5,5 +5,9 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderHeadCircle : HitCircle { + /// + /// Makes the head circle track the follow circle when the start time is reached. + /// + public bool TrackFollowCircle = true; } } From 03b7817887ea059719094f001a81c5b4f1c9ee36 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 22:12:20 +0900 Subject: [PATCH 014/102] Add flags to return to classic slider scoring --- .../Objects/Drawables/DrawableHitCircle.cs | 9 ++++++- .../Objects/Drawables/DrawableSlider.cs | 24 ++++++++++++++++++- .../Objects/Drawables/DrawableSliderHead.cs | 15 ++++++++++++ osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++++- .../Objects/SliderHeadCircle.cs | 14 ++++++++++- 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3c0260f5f5..77094f928b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - var result = HitObject.HitWindows.ResultFor(timeOffset); + var result = ResultFor(timeOffset); if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) { @@ -146,6 +146,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } + /// + /// Retrieves the for a time offset. + /// + /// The time offset. + /// The hit result, or if doesn't result in a judgement. + protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset); + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 511cbc2347..7061ce59d0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -249,7 +250,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; - ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + if (HitObject.IgnoreJudgement) + { + ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + return; + } + + // If not ignoring judgement, score proportionally based on the number of ticks hit, counting the head circle as a tick. + ApplyResult(r => + { + int totalTicks = NestedHitObjects.Count; + int hitTicks = NestedHitObjects.Count(h => h.IsHit); + double hitFraction = (double)totalTicks / hitTicks; + + if (hitTicks == totalTicks) + r.Type = HitResult.Great; + else if (hitFraction >= 0.5) + r.Type = HitResult.Ok; + else if (hitFraction > 0) + r.Type = HitResult.Meh; + else + r.Type = HitResult.Miss; + }); } public override void PlaySamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c051a9918d..08e9c5eb14 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -73,6 +76,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override HitResult ResultFor(double timeOffset) + { + Debug.Assert(HitObject != null); + + if (HitObject.JudgeAsNormalHitCircle) + return base.ResultFor(timeOffset); + + // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. + var result = base.ResultFor(timeOffset); + return result.IsHit() ? HitResult.IgnoreHit : result; + } + public Action OnShake; public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1670df24a8..e3365a8ccf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -114,6 +114,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; + /// + /// Whether this 's judgement should be ignored. + /// If false, this will be judged proportionally to the number of ticks hit. + /// + public bool IgnoreJudgement = true; + [JsonIgnore] public HitCircle HeadCircle { get; protected set; } @@ -233,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects HeadCircle.Samples = this.GetNodeSamples(0); } - public override Judgement CreateJudgement() => new OsuIgnoreJudgement(); + public override Judgement CreateJudgement() => IgnoreJudgement ? new OsuIgnoreJudgement() : new OsuJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 5fc480883a..13eac60300 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -1,13 +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 osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + namespace osu.Game.Rulesets.Osu.Objects { public class SliderHeadCircle : HitCircle { /// - /// Makes the head circle track the follow circle when the start time is reached. + /// Makes this track the follow circle when the start time is reached. + /// If false, this will be pinned to its initial position in the slider. /// public bool TrackFollowCircle = true; + + /// + /// Whether to treat this as a normal for judgement purposes. + /// If false, judgement will be ignored. + /// + public bool JudgeAsNormalHitCircle = true; + + public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new OsuIgnoreJudgement(); } } From 2f22dbe06be3645ed9c5e8f99cc015414f578256 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 22:42:50 +0900 Subject: [PATCH 015/102] Make sliders display judgements when not ignored --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 13f5960bd4..79655c33e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject?.HitObject is OsuHitObject osuObject) { - Position = osuObject.StackedPosition; + Position = osuObject.StackedEndPosition; Scale = new Vector2(osuObject.Scale); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7061ce59d0..e607163b3e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public SliderBall Ball { get; private set; } public SkinnableDrawable Body { get; private set; } - public override bool DisplayResult => false; + public override bool DisplayResult => !HitObject.IgnoreJudgement; private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; From 3b5c67a0630681b6e1e2f0d52486e241772661d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 23:08:59 +0900 Subject: [PATCH 016/102] Add OsuModClassic --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 43 +++++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + 2 files changed, 44 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs new file mode 100644 index 0000000000..5542580979 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -0,0 +1,43 @@ +// 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.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModClassic : Mod, IApplicableToHitObject + { + public override string Name => "Classic"; + + public override string Acronym => "CL"; + + public override double ScoreMultiplier => 1; + + public override IconUsage? Icon => FontAwesome.Solid.History; + + public override string Description => "Feeling nostalgic?"; + + public override bool Ranked => false; + + public void ApplyToHitObject(HitObject hitObject) + { + switch (hitObject) + { + case Slider slider: + slider.IgnoreJudgement = false; + + foreach (var head in slider.NestedHitObjects.OfType()) + { + head.TrackFollowCircle = false; + head.JudgeAsNormalHitCircle = false; + } + + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index cba0c5be14..18324a18a8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -163,6 +163,7 @@ namespace osu.Game.Rulesets.Osu { new OsuModTarget(), new OsuModDifficultyAdjust(), + new OsuModClassic() }; case ModType.Automation: From a4551dc1eeb2eb6afb9dec61752559eaa6e5632c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:31:22 +0900 Subject: [PATCH 017/102] Add object-ordered hit policy --- .../UI/ObjectOrderedHitPolicy.cs | 55 +++++++++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 +- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs new file mode 100644 index 0000000000..fdab241a9b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Ensures that s are hit in order of appearance. The classic note lock. + /// + /// Hits will be blocked until the previous s have been judged. + /// + /// + public class ObjectOrderedHitPolicy : IHitPolicy + { + private IEnumerable hitObjects; + + public void SetHitObjects(IEnumerable hitObjects) => this.hitObjects = hitObjects; + + public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged); + + public void HandleHit(DrawableHitObject hitObject) + { + } + + private IEnumerable enumerateHitObjectsUpTo(double targetTime) + { + foreach (var obj in hitObjects) + { + if (obj.HitObject.StartTime >= targetTime) + yield break; + + switch (obj) + { + case DrawableSpinner _: + continue; + + case DrawableSlider slider: + yield return slider.HeadCircle; + + break; + + default: + yield return obj; + + break; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c7900558a0..6cb890323b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; - private readonly StartTimeOrderedHitPolicy hitPolicy; + private readonly IHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new StartTimeOrderedHitPolicy(); + hitPolicy = new ObjectOrderedHitPolicy(); hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); var hitWindows = new OsuHitWindows(); From 6aece18f8dedc392a84996d1c7e4a906b72b827e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 15:23:03 +0900 Subject: [PATCH 018/102] Add OOHP tests --- .../TestSceneObjectOrderedHitPolicy.cs | 491 ++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 29 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 17 +- 3 files changed, 529 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs new file mode 100644 index 0000000000..039a4f142f --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -0,0 +1,491 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene + { + private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss + private const double late_miss_window = 500; // time after +500 is considered a miss + + /// + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTime() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); + } + + /// + /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAtFirstCircleTime() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); + } + + /// + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTime() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); + } + + /// + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 + } + + /// + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged() + { + const double time_first_circle = 1500; + const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time + } + + /// + /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. + /// + [Test] + public void TestMissSliderHeadAndHitAllSliderTicks() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); + } + + /// + /// Tests clicking hitting future slider ticks before a circle. + /// + [Test] + public void TestHitSliderTicksBeforeCircle() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(30); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); + } + + /// + /// Tests clicking a future circle before a spinner. + /// + [Test] + public void TestHitCircleBeforeSpinner() + { + const double time_spinner = 1500; + const double time_circle = 1800; + Vector2 positionCircle = Vector2.Zero; + + var hitObjects = new List + { + new TestSpinner + { + StartTime = time_spinner, + Position = new Vector2(256, 192), + EndTime = time_spinner + 1000, + }, + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + + [Test] + public void TestHitSliderHeadBeforeHitCircle() + { + const double time_circle = 1000; + const double time_slider = 1200; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) + { + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); + } + + private void addJudgementAssert(string name, Func hitObject, HitResult result) + { + AddAssert($"{name} judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + } + + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) + { + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", + () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); + } + + private ScoreAccessibleReplayPlayer currentPlayer; + private List judgementResults; + + private void performTest(List hitObjects, List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = hitObjects, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + SelectedMods.Value = new[] { new OsuModClassic() }; + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private class TestHitCircle : HitCircle + { + protected override HitWindows CreateHitWindows() => new TestHitWindows(); + } + + private class TestSlider : Slider + { + public TestSlider() + { + DefaultsApplied += _ => + { + HeadCircle.HitWindows = new TestHitWindows(); + TailCircle.HitWindows = new TestHitWindows(); + + HeadCircle.HitWindows.SetDifficulty(0); + TailCircle.HitWindows.SetDifficulty(0); + }; + } + } + + private class TestSpinner : Spinner + { + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + SpinsRequired = 1; + } + } + + private class TestHitWindows : HitWindows + { + private static readonly DifficultyRange[] ranges = + { + new DifficultyRange(HitResult.Great, 500, 500, 500), + new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), + }; + + public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; + + protected override DifficultyRange[] GetRanges() => ranges; + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 5542580979..6f41bcc0b0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -2,14 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : Mod, IApplicableToHitObject + public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableRuleset { public override string Name => "Classic"; @@ -23,21 +27,38 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => false; + [SettingSource("Disable slider head judgement", "Scores sliders proportionally to the number of ticks hit.")] + public Bindable DisableSliderHeadJudgement { get; } = new BindableBool(true); + + [SettingSource("Disable slider head tracking", "Pins slider heads at their starting position, regardless of time.")] + public Bindable DisableSliderHeadTracking { get; } = new BindableBool(true); + + [SettingSource("Disable note lock lenience", "Applies note lock to the full hit window.")] + public Bindable DisableLenientNoteLock { get; } = new BindableBool(true); + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) { case Slider slider: - slider.IgnoreJudgement = false; + slider.IgnoreJudgement = !DisableSliderHeadJudgement.Value; foreach (var head in slider.NestedHitObjects.OfType()) { - head.TrackFollowCircle = false; - head.JudgeAsNormalHitCircle = false; + head.TrackFollowCircle = !DisableSliderHeadTracking.Value; + head.JudgeAsNormalHitCircle = !DisableSliderHeadJudgement.Value; } break; } } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var osuRuleset = (DrawableOsuRuleset)drawableRuleset; + + if (!DisableLenientNoteLock.Value) + osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 6cb890323b..9bd1dc74b7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; - private readonly IHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -54,11 +53,9 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new ObjectOrderedHitPolicy(); - hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); + HitPolicy = new ObjectOrderedHitPolicy(); var hitWindows = new OsuHitWindows(); - foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded)); @@ -67,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.UI NewResult += onNewResult; } + private IHitPolicy hitPolicy; + + public IHitPolicy HitPolicy + { + get => hitPolicy; + set + { + hitPolicy = value ?? throw new ArgumentNullException(nameof(value)); + hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); + } + } + protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; From 3aa3692ed4be0a5b94e4bbb9b489063b6532b0da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 15:56:13 +0900 Subject: [PATCH 019/102] Disable snaking out when tracking is disabled --- .../Sliders/SliderCircleSelectionBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Skinning/Default/PlaySliderBody.cs | 22 ++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs index a0392fe536..dec9cd8622 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.Update(); - CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle); + CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle); } // Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e3365a8ccf..01694a838b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects public bool IgnoreJudgement = true; [JsonIgnore] - public HitCircle HeadCircle { get; protected set; } + public SliderHeadCircle HeadCircle { get; protected set; } [JsonIgnore] public SliderTailCircle TailCircle { get; protected set; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index e77c93c721..e9b4bb416c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default [Resolved(CanBeNull = true)] private OsuRulesetConfigManager config { get; set; } + private readonly Bindable snakingOut = new Bindable(); + [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { @@ -35,11 +37,29 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true); + SnakingOut.BindTo(snakingOut); config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn); - config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut); + config?.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; + + drawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableObject); + } + + private void onHitObjectApplied(DrawableHitObject obj) + { + var drawableSlider = (DrawableSlider)obj; + if (drawableSlider.HitObject == null) + return; + + if (!drawableSlider.HitObject.HeadCircle.TrackFollowCircle) + { + // When not tracking the follow circle, force the path to not snake out as it looks better that way. + SnakingOut.UnbindFrom(snakingOut); + SnakingOut.Value = false; + } } private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour) From ee3367d7c549acecb778c652365c290e3e186e5b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 16:59:13 +0900 Subject: [PATCH 020/102] Add classic slider ball tracking --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 17 ++++++++++++++++- .../Skinning/Default/SliderBall.cs | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 6f41bcc0b0..df3afb7063 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -1,19 +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 System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableRuleset + public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset { public override string Name => "Classic"; @@ -36,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Disable note lock lenience", "Applies note lock to the full hit window.")] public Bindable DisableLenientNoteLock { get; } = new BindableBool(true); + [SettingSource("Disable exact slider follow circle tracking", "Makes the slider follow circle track its final size at all times.")] + public Bindable DisableExactFollowCircleTracking { get; } = new BindableBool(true); + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) @@ -60,5 +66,14 @@ namespace osu.Game.Rulesets.Osu.Mods if (!DisableLenientNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var obj in drawables) + { + if (obj is DrawableSlider slider) + slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + } + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs index a96beb66d4..da3debbd42 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs @@ -31,6 +31,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default set => ball.Colour = value; } + /// + /// Whether to track accurately to the visual size of this . + /// If false, tracking will be performed at the final scale at all times. + /// + public bool TrackVisualSize = true; + private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; private readonly Drawable ball; @@ -94,7 +100,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default tracking = value; - followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); + if (TrackVisualSize) + followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); + else + { + // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. + followCircle.ScaleTo(tracking ? 2.4f : 1f); + } + followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); } } From a5855f5d28f8e7db6b9d4038806e5d70b38c3a7f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 17:33:48 +0900 Subject: [PATCH 021/102] Move follow circle tracking to DrawableSliderHead --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 15 ++++++++++----- .../Objects/Drawables/DrawableSliderHead.cs | 8 +++++++- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 6 ------ .../Skinning/Default/PlaySliderBody.cs | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index df3afb7063..8cd6676f9d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -50,10 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods slider.IgnoreJudgement = !DisableSliderHeadJudgement.Value; foreach (var head in slider.NestedHitObjects.OfType()) - { - head.TrackFollowCircle = !DisableSliderHeadTracking.Value; head.JudgeAsNormalHitCircle = !DisableSliderHeadJudgement.Value; - } break; } @@ -71,8 +68,16 @@ namespace osu.Game.Rulesets.Osu.Mods { foreach (var obj in drawables) { - if (obj is DrawableSlider slider) - slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + switch (obj) + { + case DrawableSlider slider: + slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + break; + + case DrawableSliderHead head: + head.TrackFollowCircle = !DisableSliderHeadTracking.Value; + break; + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 08e9c5eb14..ee1df00ef7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -22,6 +22,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; + /// + /// Makes this track the follow circle when the start time is reached. + /// If false, this will be pinned to its initial position in the slider. + /// + public bool TrackFollowCircle = true; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -66,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Debug.Assert(Slider != null); Debug.Assert(HitObject != null); - if (HitObject.TrackFollowCircle) + if (TrackFollowCircle) { double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 13eac60300..28e57567cb 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -8,12 +8,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderHeadCircle : HitCircle { - /// - /// Makes this track the follow circle when the start time is reached. - /// If false, this will be pinned to its initial position in the slider. - /// - public bool TrackFollowCircle = true; - /// /// Whether to treat this as a normal for judgement purposes. /// If false, judgement will be ignored. diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index e9b4bb416c..8eb2714c04 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (drawableSlider.HitObject == null) return; - if (!drawableSlider.HitObject.HeadCircle.TrackFollowCircle) + if (!drawableSlider.HeadCircle.TrackFollowCircle) { // When not tracking the follow circle, force the path to not snake out as it looks better that way. SnakingOut.UnbindFrom(snakingOut); From f6d08f54e6e950f4784087012fe27cd3e566a22c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 5 Feb 2021 21:19:13 +0100 Subject: [PATCH 022/102] Use the oldest user config file available when there happens to be multiple config files available. --- osu.Game/IO/StableStorage.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index f86b18c724..614e548d93 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -35,7 +35,11 @@ namespace osu.Game.IO { var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - var configFile = GetFiles(".", "osu!.*.cfg").SingleOrDefault(); + // enumerate the user config files available in case the user migrated their files from another pc / operating system. + var foundConfigFiles = GetFiles(".", "osu!.*.cfg"); + + // if more than one config file is found, let's use the oldest one (where the username in the filename doesn't match the local username). + var configFile = foundConfigFiles.Count() > 1 ? foundConfigFiles.FirstOrDefault(filename => !filename[5..^4].Contains(Environment.UserName, StringComparison.Ordinal)) : foundConfigFiles.FirstOrDefault(); if (configFile == null) return songsDirectoryPath; From 68c20a2a3705dcc0995303e83a4312164d9fd98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:19:07 +0100 Subject: [PATCH 023/102] Allow autoplay score generation to access mod list --- osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs | 3 ++- osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs | 3 ++- .../TestSceneMissHitWindowJudgements.cs | 4 +++- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs | 3 ++- osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs | 2 +- osu.Game/Rulesets/Mods/ModAutoplay.cs | 12 +++++++++++- osu.Game/Rulesets/Mods/ModCinema.cs | 4 +++- 12 files changed, 34 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index 692e63fa69..e1eceea606 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, Replay = new CatchAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs index 3bc1ee5bf5..d53d019e90 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, Replay = new CatchAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index c05e979e9a..105d88129c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs index 02c1fc1b79..064c55ed8d 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 39deba2f57..f73649fcd9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Replays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -65,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestAutoMod : OsuModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new MissingAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index bea2bbcb32..454c94cd96 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new OsuAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 5d9a524577..99e5568bb3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new OsuAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 5b890b3d03..64e59b64d0 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, Replay = new TaikoAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs index 71aa007d3b..00f0c8e321 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.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.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, Replay = new TaikoAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index 3a71d4ca54..f94e122b30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); - return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap)); + return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap, Array.Empty())); } protected override void AddCheckSteps() diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 945dd444be..748c7272f4 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -15,7 +16,11 @@ namespace osu.Game.Rulesets.Mods public abstract class ModAutoplay : ModAutoplay, IApplicableToDrawableRuleset where T : HitObject { - public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) => drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); + } } public abstract class ModAutoplay : Mod, IApplicableFailOverride @@ -35,6 +40,11 @@ namespace osu.Game.Rulesets.Mods public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; + [Obsolete("Use the mod-supporting override")] // can be removed 20210731 public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() }; + +#pragma warning disable 618 + public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => CreateReplayScore(beatmap); +#pragma warning restore 618 } } diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index bee9e56edd..16e6400f23 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.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.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -14,7 +15,8 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); + var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); // AlwaysPresent required for hitsounds drawableRuleset.Playfield.AlwaysPresent = true; From 7daeacaff230b58a13847db3727918cf7da38d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:43:16 +0100 Subject: [PATCH 024/102] Add and implement IApplicableToRate interface --- osu.Game/Rulesets/Mods/IApplicableToRate.cs | 20 ++++++++++++++++++++ osu.Game/Rulesets/Mods/ModRateAdjust.cs | 4 +++- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 19 ++++++++++++------- 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToRate.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToRate.cs b/osu.Game/Rulesets/Mods/IApplicableToRate.cs new file mode 100644 index 0000000000..f613867132 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToRate.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Interface that should be implemented by mods that affect the track playback speed, + /// and in turn, values of the track rate. + /// + public interface IApplicableToRate : IApplicableToAudio + { + /// + /// Returns the playback rate at after this mod is applied. + /// + /// The time instant at which the playback rate is queried. + /// The playback rate before applying this mod. + /// The playback rate after applying this mod. + double ApplyToRate(double time, double rate = 1); + } +} diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 2150b0fb68..b016a6d43b 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Audio; namespace osu.Game.Rulesets.Mods { - public abstract class ModRateAdjust : Mod, IApplicableToAudio + public abstract class ModRateAdjust : Mod, IApplicableToRate { public abstract BindableNumber SpeedChange { get; } @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mods sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } + public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b6916c838e..7e801c3024 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { - public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio + public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToRate { /// /// The point in the beatmap at which the final ramping rate should be reached. @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mods protected ModTimeRamp() { // for preview purpose at song select. eventually we'll want to be able to update every frame. - FinalRate.BindValueChanged(val => applyRateAdjustment(1), true); + FinalRate.BindValueChanged(val => applyRateAdjustment(double.PositiveInfinity), true); AdjustPitch.BindValueChanged(applyPitchAdjustment); } @@ -75,17 +75,22 @@ namespace osu.Game.Rulesets.Mods finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart); } + public double ApplyToRate(double time, double rate = 1) + { + double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime); + double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); + return rate * ramp; + } + public virtual void Update(Playfield playfield) { - applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime)); + applyRateAdjustment(track.CurrentTime); } /// - /// Adjust the rate along the specified ramp + /// Adjust the rate along the specified ramp. /// - /// The amount of adjustment to apply (from 0..1). - private void applyRateAdjustment(double amount) => - SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); + private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time); private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { From 3fabe247b09a32ccf1fb6a652356875ce1c63b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:59:35 +0100 Subject: [PATCH 025/102] Allow OsuModGenerator to accept a mod list --- .../TestSceneMissHitWindowJudgements.cs | 6 +++--- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 2 +- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 6 ++++-- osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs | 3 ++- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index f73649fcd9..af67ab5839 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, - Replay = new MissingAutoGenerator(beatmap).Generate() + Replay = new MissingAutoGenerator(beatmap, mods).Generate() }; } @@ -78,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.Tests { public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap; - public MissingAutoGenerator(IBeatmap beatmap) - : base(beatmap) + public MissingAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) + : base(beatmap, mods) { } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 8c819c4773..59a5295858 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -62,7 +62,8 @@ namespace osu.Game.Rulesets.Osu.Mods inputManager.AllowUserCursorMovement = false; // Generate the replay frames the cursor should follow - replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast().ToList(); + var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); + replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, mods).Generate().Frames.Cast().ToList(); } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 454c94cd96..3b1f271d41 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, - Replay = new OsuAutoGenerator(beatmap).Generate() + Replay = new OsuAutoGenerator(beatmap, mods).Generate() }; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 99e5568bb3..df06988b70 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, - Replay = new OsuAutoGenerator(beatmap).Generate() + Replay = new OsuAutoGenerator(beatmap, mods).Generate() }; } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 954a217473..e4b6f6425d 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -6,10 +6,12 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; using osu.Game.Replays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Scoring; @@ -49,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.Replays #region Construction / Initialisation - public OsuAutoGenerator(IBeatmap beatmap) - : base(beatmap) + public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) + : base(beatmap, mods) { // Already superhuman, but still somewhat realistic reactionTime = ApplyModsToRate(100); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 3356a0fbe0..f88594a3ee 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -6,6 +6,7 @@ using osu.Game.Beatmaps; using System; using System.Collections.Generic; using osu.Game.Replays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Replays protected Replay Replay; protected List Frames => Replay.Frames; - protected OsuAutoGeneratorBase(IBeatmap beatmap) + protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap) { Replay = new Replay(); From 0e1ec703d33907268dbb2acc49f3e8e19318ae2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:56:03 +0100 Subject: [PATCH 026/102] Use IApplicableToRate in osu! auto generator --- .../Replays/OsuAutoGenerator.cs | 42 ++++++++++------- .../Replays/OsuAutoGeneratorBase.cs | 47 +++++++++++++++---- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index e4b6f6425d..693943a08a 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -35,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays #region Constants - /// - /// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it. - /// - private readonly double reactionTime; - private readonly HitWindows defaultHitWindows; /// @@ -54,9 +49,6 @@ namespace osu.Game.Rulesets.Osu.Replays public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap, mods) { - // Already superhuman, but still somewhat realistic - reactionTime = ApplyModsToRate(100); - defaultHitWindows = new OsuHitWindows(); defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); } @@ -242,7 +234,7 @@ namespace osu.Game.Rulesets.Osu.Replays 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); + double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt)); if (waitTime > lastFrame.Time) { @@ -252,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Replays Vector2 lastPosition = lastFrame.Position; - double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time); + double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime); // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. if (timeDifference > 0 && // Sanity checks @@ -260,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Replays timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. { // Perform eased movement - for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) + for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time)) { Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); @@ -274,6 +266,14 @@ namespace osu.Game.Rulesets.Osu.Replays } } + /// + /// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it. + /// + /// + /// Already superhuman, but still somewhat realistic. + /// + private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100); + // Add frames to click the hitobject private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection) { @@ -343,17 +343,23 @@ namespace osu.Game.Rulesets.Osu.Replays float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); double t; + double previousFrame = h.StartTime; - for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay) + for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame)) { - t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; + t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection; + angle += (float)t / 20; - Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); - AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); + Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); + AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action)); + + previousFrame = nextFrame; } - t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection; - Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); + t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection; + angle += (float)t / 20; + + Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); @@ -361,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays break; case Slider slider: - for (double j = FrameDelay; j < slider.Duration; j += FrameDelay) + for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j)) { Vector2 pos = slider.StackedPositionAt(j / slider.Duration); AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index f88594a3ee..1cb3208c30 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -5,6 +5,7 @@ using osuTK; using osu.Game.Beatmaps; using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.UI; @@ -23,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays public const float SPIN_RADIUS = 50; - /// - /// The time in ms between each ReplayFrame. - /// - protected readonly double FrameDelay; - #endregion #region Construction / Initialisation protected Replay Replay; protected List Frames => Replay.Frames; + private readonly IReadOnlyList timeAffectingMods; protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap) { Replay = new Replay(); - // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. - FrameDelay = ApplyModsToRate(1000.0 / 60.0); + timeAffectingMods = mods.OfType().ToList(); } #endregion #region Utilities - protected double ApplyModsToTime(double v) => v; - protected double ApplyModsToRate(double v) => v; + /// + /// Returns the real duration of time between and + /// after applying rate-affecting mods. + /// + /// + /// This method should only be used when and are very close. + /// That is because the track rate might be changing with time, + /// and the method used here is a rough instantaneous approximation. + /// + /// The start time of the time delta, in original track time. + /// The end time of the time delta, in original track time. + protected double ApplyModsToTimeDelta(double startTime, double endTime) + { + double delta = endTime - startTime; + + foreach (var mod in timeAffectingMods) + delta /= mod.ApplyToRate(startTime); + + return delta; + } + + protected double ApplyModsToRate(double time, double rate) + { + foreach (var mod in timeAffectingMods) + rate = mod.ApplyToRate(time, rate); + return rate; + } + + /// + /// Calculates the interval after which the next should be generated, + /// in milliseconds. + /// + /// The time of the previous frame. + protected double GetFrameDelay(double time) + => ApplyModsToRate(time, 1000.0 / 60); private class ReplayFrameComparer : IComparer { From 0229851c9ca93570e68dbe056cc15d20099b1cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 7 Feb 2021 19:02:09 +0100 Subject: [PATCH 027/102] Apply rounding to ModTimeRamp to improve SPM consistency --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 7e801c3024..330945d3d3 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -79,7 +79,9 @@ namespace osu.Game.Rulesets.Mods { double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime); double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); - return rate * ramp; + + // round the end result to match the bindable SpeedChange's precision, in case this is called externally. + return rate * Math.Round(ramp, 2); } public virtual void Update(Playfield playfield) From 0df15b4d7a4361368d1504ec18695902b9969d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 7 Feb 2021 19:25:33 +0100 Subject: [PATCH 028/102] Add test coverage --- .../Mods/TestSceneOsuModAutoplay.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs new file mode 100644 index 0000000000..856b6554b9 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.Osu.UI; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModAutoplay : OsuModTestScene + { + [Test] + public void TestSpmUnaffectedByRateAdjust() + => runSpmTest(new OsuModDaycore + { + SpeedChange = { Value = 0.88 } + }); + + [Test] + public void TestSpmUnaffectedByTimeRamp() + => runSpmTest(new ModWindUp + { + InitialRate = { Value = 0.7 }, + FinalRate = { Value = 1.3 } + }); + + private void runSpmTest(Mod mod) + { + SpinnerSpmCounter spmCounter = null; + + CreateModTest(new ModTestData + { + Autoplay = true, + Mod = mod, + Beatmap = new Beatmap + { + HitObjects = + { + new Spinner + { + Duration = 2000, + Position = OsuPlayfield.BASE_SIZE / 2 + } + } + }, + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1 + }); + + AddUntilStep("fetch SPM counter", () => + { + spmCounter = this.ChildrenOfType().SingleOrDefault(); + return spmCounter != null; + }); + + AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5)); + } + } +} From 2218247b21d09a8317ae84dd4dffa1bbf7a744c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 11:07:50 +0900 Subject: [PATCH 029/102] Override mod type --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 8cd6676f9d..863dc05216 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => false; + public override ModType Type => ModType.Conversion; + [SettingSource("Disable slider head judgement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable DisableSliderHeadJudgement { get; } = new BindableBool(true); From d955200e0718db14fa4b5ea13e6355e5b7134983 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 11:10:07 +0900 Subject: [PATCH 030/102] Prevent invalid hit results for ignored slider heads --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index ee1df00ef7..87cfa47091 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. var result = base.ResultFor(timeOffset); - return result.IsHit() ? HitResult.IgnoreHit : result; + return result.IsHit() ? HitResult.IgnoreHit : HitResult.IgnoreMiss; } public Action OnShake; From f0dfa9f8f397269571b96d1165f03f36a555bc8e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 8 Feb 2021 11:12:25 +0100 Subject: [PATCH 031/102] Use the newest config file available (where the local username matches the filename) --- osu.Game/IO/StableStorage.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index 614e548d93..ccc6f9c311 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -35,11 +35,7 @@ namespace osu.Game.IO { var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - // enumerate the user config files available in case the user migrated their files from another pc / operating system. - var foundConfigFiles = GetFiles(".", "osu!.*.cfg"); - - // if more than one config file is found, let's use the oldest one (where the username in the filename doesn't match the local username). - var configFile = foundConfigFiles.Count() > 1 ? foundConfigFiles.FirstOrDefault(filename => !filename[5..^4].Contains(Environment.UserName, StringComparison.Ordinal)) : foundConfigFiles.FirstOrDefault(); + var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault(); if (configFile == null) return songsDirectoryPath; From b96a594546b38a866c7e661b707de04518407baf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 15:11:58 +0900 Subject: [PATCH 032/102] Remove unnecessary initial call to HitObjectApplied bound method Was causing test failures. Looks to be unnecessary on a check of when HitObjectApplied is invoked. --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index 8eb2714c04..f9b8ffca7b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -45,7 +45,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; drawableObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(drawableObject); } private void onHitObjectApplied(DrawableHitObject obj) From 806324b196607d6d918f792289d4043100944904 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 14:53:29 +0900 Subject: [PATCH 033/102] Allow overriding of Overlay pop-in and pop-out samples --- osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 41fd37a0d7..ee99a39523 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -20,6 +20,8 @@ namespace osu.Game.Graphics.Containers { private SampleChannel samplePopIn; private SampleChannel samplePopOut; + protected virtual string PopInSampleName => "UI/overlay-pop-in"; + protected virtual string PopOutSampleName => "UI/overlay-pop-out"; protected override bool BlockNonPositionalInput => true; @@ -40,8 +42,8 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader(true)] private void load(AudioManager audio) { - samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in"); - samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out"); + samplePopIn = audio.Samples.Get(PopInSampleName); + samplePopOut = audio.Samples.Get(PopOutSampleName); } protected override void LoadComplete() From 3eda78c363def6589adc0a84aee6c7795cd72d76 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 14:57:46 +0900 Subject: [PATCH 034/102] Use unique samples for Dialog, NowPlaying, SettingsPanel and WaveOverlay pop-in/pop-out --- osu.Game/Overlays/DialogOverlay.cs | 3 +++ osu.Game/Overlays/NowPlayingOverlay.cs | 3 +++ osu.Game/Overlays/SettingsPanel.cs | 2 ++ osu.Game/Overlays/WaveOverlayContainer.cs | 2 ++ 4 files changed, 10 insertions(+) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 9f9dbdbaf1..4cc17a4c14 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -14,6 +14,9 @@ namespace osu.Game.Overlays { private readonly Container dialogContainer; + protected override string PopInSampleName => "UI/dialog-pop-in"; + protected override string PopOutSampleName => "UI/dialog-pop-out"; + public PopupDialog CurrentDialog { get; private set; } public DialogOverlay() diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 5c16a6e5c4..2866d2ad6d 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -51,6 +51,9 @@ namespace osu.Game.Overlays private Container dragContainer; private Container playerContainer; + protected override string PopInSampleName => "UI/now-playing-pop-in"; + protected override string PopOutSampleName => "UI/now-playing-pop-out"; + /// /// Provide a source for the toolbar height. /// diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 7a5a586f67..f1270f750e 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -40,6 +40,8 @@ namespace osu.Game.Overlays private SeekLimitedSearchTextBox searchTextBox; + protected override string PopInSampleName => "UI/settings-pop-in"; + /// /// Provide a source for the toolbar height. /// diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index d0fa9987d5..52ae4dbdbb 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays protected override bool StartHidden => true; + protected override string PopInSampleName => "UI/wave-pop-in"; + protected WaveOverlayContainer() { AddInternal(Waves = new WaveContainer From 22995c216deb26b16ad5e307b35cc5c69ff54260 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 14:59:30 +0900 Subject: [PATCH 035/102] Use unique sample for edit button click (ButtonSystem) --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index f400b2114b..c6774127c1 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Menu buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new Button(@"edit", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D)); if (host.CanExit) From 73ab1b2b21f2eaaf40884d6d674d22c08031bfd7 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 15:01:22 +0900 Subject: [PATCH 036/102] Add pitch randomisation to HoverSounds on-hover sample playback --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index a1d06711db..21aae1b861 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Framework.Utils; namespace osu.Game.Graphics.UserInterface { @@ -49,9 +50,11 @@ namespace osu.Game.Graphics.UserInterface { bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - if (enoughTimePassedSinceLastPlayback) + if (enoughTimePassedSinceLastPlayback && sampleHover != null) { - sampleHover?.Play(); + sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); + sampleHover.Play(); + lastPlaybackTime.Value = Time.Current; } From 4e2ab0bad2c1aed6c9abc75250d1160ae87218f4 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 15:02:17 +0900 Subject: [PATCH 037/102] Use a separate sample set for Toolbar buttons --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 5 ++++- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index a1d06711db..4a79d1fbec 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -68,6 +68,9 @@ namespace osu.Game.Graphics.UserInterface Normal, [Description("-softer")] - Soft + Soft, + + [Description("-toolbar")] + Toolbar } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 49b9c62d85..83f2bdf6cb 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar private KeyBindingStore keyBindings { get; set; } protected ToolbarButton() - : base(HoverSampleSet.Loud) + : base(HoverSampleSet.Toolbar) { Width = Toolbar.HEIGHT; RelativeSizeAxes = Axes.Y; From bc7f4a4f881b04feb4ac1c23b2d1fc5aba1fada5 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 20:16:22 +0900 Subject: [PATCH 038/102] Use a single sample for CarouselHeader on-hover with randomised pitch instead of multiple samples --- osu.Game/Screens/Select/Carousel/CarouselHeader.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index f1120f55a6..4f53a6e202 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { - sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); + sampleHover = audio.Samples.Get("SongSelect/song-ping"); hoverLayer.Colour = colours.Blue.Opacity(0.1f); } @@ -99,7 +99,11 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnHover(HoverEvent e) { - sampleHover?.Play(); + if (sampleHover != null) + { + sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2); + sampleHover.Play(); + } hoverLayer.FadeIn(100, Easing.OutQuint); return base.OnHover(e); From 625eb78a118566dc72161da36c9b5997d10309e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 17:59:52 +0900 Subject: [PATCH 039/102] Simplify with an early exit for null sample --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 21aae1b861..5d6d0896fd 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -48,9 +48,12 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { + if (sampleHover == null) + return false; + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - if (enoughTimePassedSinceLastPlayback && sampleHover != null) + if (enoughTimePassedSinceLastPlayback) { sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); sampleHover.Play(); @@ -58,7 +61,7 @@ namespace osu.Game.Graphics.UserInterface lastPlaybackTime.Value = Time.Current; } - return base.OnHover(e); + return false; } } From 996f1098f6e7054e97150421ead8188147f5aa09 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 10 Feb 2021 18:14:32 +0900 Subject: [PATCH 040/102] Use alternate sample on the downbeat while hovering OsuLogo --- osu.Game/Screens/Menu/OsuLogo.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 68d23e1a32..1d0af30275 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -45,6 +45,7 @@ namespace osu.Game.Screens.Menu private SampleChannel sampleClick; private SampleChannel sampleBeat; + private SampleChannel sampleDownbeat; private readonly Container colourAndTriangles; private readonly Triangles triangles; @@ -259,6 +260,7 @@ namespace osu.Game.Screens.Menu { sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); + sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); @@ -281,7 +283,15 @@ namespace osu.Game.Screens.Menu if (beatIndex < 0) return; if (IsHovered) - this.Delay(early_activation).Schedule(() => sampleBeat.Play()); + { + this.Delay(early_activation).Schedule(() => + { + if (beatIndex % (int)timingPoint.TimeSignature == 0) + sampleDownbeat.Play(); + else + sampleBeat.Play(); + }); + } logoBeatContainer .ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then() From cf06684ad121d58548d6cf0b9e87a439167dbc57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:38:31 +0900 Subject: [PATCH 041/102] Judge heads as slider ticks instead --- .../Judgements/SliderTickJudgement.cs | 12 ++++++++++++ .../Objects/Drawables/DrawableSliderHead.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 5 ----- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs new file mode 100644 index 0000000000..a088696784 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class SliderTickJudgement : OsuJudgement + { + public override HitResult MaxResult => HitResult.LargeTickHit; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 87cfa47091..c3759b6a34 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. var result = base.ResultFor(timeOffset); - return result.IsHit() ? HitResult.IgnoreHit : HitResult.IgnoreMiss; + return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; } public Action OnShake; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 28e57567cb..5672283230 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -10,10 +10,10 @@ namespace osu.Game.Rulesets.Osu.Objects { /// /// Whether to treat this as a normal for judgement purposes. - /// If false, judgement will be ignored. + /// If false, this will be judged as a instead. /// public bool JudgeAsNormalHitCircle = true; - public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new OsuIgnoreJudgement(); + public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index a427ee1955..725dbe81fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; public override Judgement CreateJudgement() => new SliderTickJudgement(); - - public class SliderTickJudgement : OsuJudgement - { - public override HitResult MaxResult => HitResult.LargeTickHit; - } } } From 6730c4c58b22ce2e753ac0f4b7055b5b87f62cde Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:41:28 +0900 Subject: [PATCH 042/102] Apply review comments (user explanations + property names) --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 863dc05216..642da87693 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -32,27 +32,27 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Conversion; - [SettingSource("Disable slider head judgement", "Scores sliders proportionally to the number of ticks hit.")] - public Bindable DisableSliderHeadJudgement { get; } = new BindableBool(true); + [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] + public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); - [SettingSource("Disable slider head tracking", "Pins slider heads at their starting position, regardless of time.")] - public Bindable DisableSliderHeadTracking { get; } = new BindableBool(true); + [SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")] + public Bindable NoSliderHeadMovement { get; } = new BindableBool(true); - [SettingSource("Disable note lock lenience", "Applies note lock to the full hit window.")] - public Bindable DisableLenientNoteLock { get; } = new BindableBool(true); + [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] + public Bindable ClassicNoteLock { get; } = new BindableBool(true); - [SettingSource("Disable exact slider follow circle tracking", "Makes the slider follow circle track its final size at all times.")] - public Bindable DisableExactFollowCircleTracking { get; } = new BindableBool(true); + [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")] + public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true); public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) { case Slider slider: - slider.IgnoreJudgement = !DisableSliderHeadJudgement.Value; + slider.IgnoreJudgement = !NoSliderHeadAccuracy.Value; foreach (var head in slider.NestedHitObjects.OfType()) - head.JudgeAsNormalHitCircle = !DisableSliderHeadJudgement.Value; + head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value; break; } @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var osuRuleset = (DrawableOsuRuleset)drawableRuleset; - if (!DisableLenientNoteLock.Value) + if (!ClassicNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } @@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Osu.Mods switch (obj) { case DrawableSlider slider: - slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + slider.Ball.TrackVisualSize = !FixedFollowCircleHitArea.Value; break; case DrawableSliderHead head: - head.TrackFollowCircle = !DisableSliderHeadTracking.Value; + head.TrackFollowCircle = !NoSliderHeadMovement.Value; break; } } From 18a29dcb9664075e4fd1dadfc57157cbcc2fc21a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:42:13 +0900 Subject: [PATCH 043/102] Rename bindable member, reorder binds --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index f9b8ffca7b..b9cd176c63 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default [Resolved(CanBeNull = true)] private OsuRulesetConfigManager config { get; set; } - private readonly Bindable snakingOut = new Bindable(); + private readonly Bindable configSnakingOut = new Bindable(); [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) @@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true); - SnakingOut.BindTo(snakingOut); config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn); - config?.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); + config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut); + + SnakingOut.BindTo(configSnakingOut); BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; @@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (!drawableSlider.HeadCircle.TrackFollowCircle) { // When not tracking the follow circle, force the path to not snake out as it looks better that way. - SnakingOut.UnbindFrom(snakingOut); + SnakingOut.UnbindFrom(configSnakingOut); SnakingOut.Value = false; } } From 9519b7f7c16e234d119ecaf65e3ff019ad84c400 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:43:14 +0900 Subject: [PATCH 044/102] Adjust comment --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index b9cd176c63..4dd7b2d69c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (drawableSlider.HitObject == null) return; + // When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way. if (!drawableSlider.HeadCircle.TrackFollowCircle) { - // When not tracking the follow circle, force the path to not snake out as it looks better that way. SnakingOut.UnbindFrom(configSnakingOut); SnakingOut.Value = false; } From 2fcc4213e16f8a9ebc33d91474fe3c0a632cf36c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:46:26 +0900 Subject: [PATCH 045/102] Rename IgnoreJudgement -> OnlyJudgeNestedObjects --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 642da87693..8e533854c0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case Slider slider: - slider.IgnoreJudgement = !NoSliderHeadAccuracy.Value; + slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value; foreach (var head in slider.NestedHitObjects.OfType()) head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index e607163b3e..13057d7a9a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public SliderBall Ball { get; private set; } public SkinnableDrawable Body { get; private set; } - public override bool DisplayResult => !HitObject.IgnoreJudgement; + public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects; private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; @@ -250,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; - if (HitObject.IgnoreJudgement) + if (HitObject.OnlyJudgeNestedObjects) { ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 01694a838b..332163454a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -115,10 +115,10 @@ namespace osu.Game.Rulesets.Osu.Objects public double TickDistanceMultiplier = 1; /// - /// Whether this 's judgement should be ignored. - /// If false, this will be judged proportionally to the number of ticks hit. + /// Whether this 's judgement is fully handled by its nested s. + /// If false, this will be judged proportionally to the number of nested s hit. /// - public bool IgnoreJudgement = true; + public bool OnlyJudgeNestedObjects = true; [JsonIgnore] public SliderHeadCircle HeadCircle { get; protected set; } @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects HeadCircle.Samples = this.GetNodeSamples(0); } - public override Judgement CreateJudgement() => IgnoreJudgement ? new OsuIgnoreJudgement() : new OsuJudgement(); + public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } From a16f4cee3a0d44bbcdf69adb4949f2d3a425efd1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:52:39 +0900 Subject: [PATCH 046/102] Adjust DrawableSlider comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 13057d7a9a..921139c4e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -250,13 +250,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; + // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. + // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). if (HitObject.OnlyJudgeNestedObjects) { ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); return; } - // If not ignoring judgement, score proportionally based on the number of ticks hit, counting the head circle as a tick. + // Otherwise, if this slider is also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. ApplyResult(r => { int totalTicks = NestedHitObjects.Count; From 6bf40170db22f31cea0c040824a218794d236718 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:53:23 +0900 Subject: [PATCH 047/102] Rename SliderBall flag --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 8e533854c0..17b0b18b52 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (obj) { case DrawableSlider slider: - slider.Ball.TrackVisualSize = !FixedFollowCircleHitArea.Value; + slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value; break; case DrawableSliderHead head: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs index da3debbd42..82b677e12c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// Whether to track accurately to the visual size of this . /// If false, tracking will be performed at the final scale at all times. /// - public bool TrackVisualSize = true; + public bool InputTracksVisualSize = true; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default tracking = value; - if (TrackVisualSize) + if (InputTracksVisualSize) followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); else { From 0dcdad98397453b439cab67421e29ecb3ca13f00 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:04:23 +0900 Subject: [PATCH 048/102] Adjust comment for DrawableSliderHead --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c3759b6a34..01c0d988ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (HitObject.JudgeAsNormalHitCircle) return base.ResultFor(timeOffset); - // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. + // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring. var result = base.ResultFor(timeOffset); return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; } From 393cd6c74a354919dc0b410d0bea38b8701bd032 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:39:47 +0900 Subject: [PATCH 049/102] Add helper class for tracking changes to mod settings --- .../Configuration/SettingSourceAttribute.cs | 27 ++++++++++++++++ .../Screens/Select/Details/AdvancedStats.cs | 31 ++++++------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 50069be4b2..00c322065a 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; namespace osu.Game.Configuration { @@ -140,4 +141,30 @@ namespace osu.Game.Configuration return orderedRelative.Concat(unordered); } } + + public class ModSettingChangeTracker : IDisposable + { + public Action SettingChanged; + + private readonly List references = new List(); + + public ModSettingChangeTracker(IEnumerable mods) + { + foreach (var mod in mods) + { + foreach (var setting in mod.CreateSettingsControls().OfType()) + { + setting.SettingChanged += () => SettingChanged?.Invoke(mod); + references.Add(setting); + } + } + } + + public void Dispose() + { + foreach (var r in references) + r.Dispose(); + references.Clear(); + } + } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 44d908fc46..7966ec4240 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -18,7 +18,6 @@ using System.Threading; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Details @@ -83,32 +82,22 @@ namespace osu.Game.Screens.Select.Details mods.BindValueChanged(modsChanged, true); } - private readonly List references = new List(); + private ModSettingChangeTracker settingChangeTracker; + private ScheduledDelegate debouncedStatisticsUpdate; private void modsChanged(ValueChangedEvent> mods) { - // TODO: find a more permanent solution for this if/when it is needed in other components. - // this is generating drawables for the only purpose of storing bindable references. - foreach (var r in references) - r.Dispose(); + settingChangeTracker?.Dispose(); - references.Clear(); - - ScheduledDelegate debounce = null; - - foreach (var mod in mods.NewValue.OfType()) + settingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + settingChangeTracker.SettingChanged += m => { - foreach (var setting in mod.CreateSettingsControls().OfType()) - { - setting.SettingChanged += () => - { - debounce?.Cancel(); - debounce = Scheduler.AddDelayed(updateStatistics, 100); - }; + if (!(m is IApplicableToDifficulty)) + return; - references.Add(setting); - } - } + debouncedStatisticsUpdate?.Cancel(); + debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100); + }; updateStatistics(); } From 7827e991b2dda30e265c3fc210e3828698d9d00b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:43:39 +0900 Subject: [PATCH 050/102] Also clear event on dispose --- osu.Game/Configuration/SettingSourceAttribute.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 00c322065a..04b8f8e962 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -162,6 +162,8 @@ namespace osu.Game.Configuration public void Dispose() { + SettingChanged = null; + foreach (var r in references) r.Dispose(); references.Clear(); From 822c66033f0d9cb2c2dbee0d138cdb67bc1fdd3c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:56:59 +0900 Subject: [PATCH 051/102] Add local-user freemod configuration --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 9 +++++++- .../OnlinePlay/FreeModSelectOverlay.cs | 5 +++-- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 21 +++++++++++++++---- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 93fe693937..488c0659f4 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -51,6 +51,11 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool Stacked => true; + /// + /// Whether configurable s can be configured by the local user. + /// + protected virtual bool AllowConfiguration => true; + [NotNull] private Func isValidMod = m => true; @@ -300,6 +305,7 @@ namespace osu.Game.Overlays.Mods Text = "Customisation", Action = () => ModSettingsContainer.ToggleVisibility(), Enabled = { Value = false }, + Alpha = AllowConfiguration ? 1 : 0, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, @@ -509,7 +515,8 @@ namespace osu.Game.Overlays.Mods OnModSelected(selectedMod); - if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); + if (selectedMod.RequiresConfiguration && AllowConfiguration) + ModSettingsContainer.Show(); } else { diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 7bc226bb3f..ab7be13479 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -20,17 +20,18 @@ namespace osu.Game.Screens.OnlinePlay { protected override bool Stacked => false; + protected override bool AllowConfiguration => false; + public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m); + set => base.IsValidMod = m => m.HasImplementation && !(m is ModAutoplay) && value(m); } public FreeModSelectOverlay() { IsValidMod = m => true; - CustomiseButton.Alpha = 0; MultiplierSection.Alpha = 0; DeselectAllButton.Alpha = 0; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 84e8849726..f17d97c3fd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -59,6 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration; + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5f2f1366f7..7edba3231c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -12,6 +12,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; @@ -314,12 +316,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } + private ModSettingChangeTracker modSettingChangeTracker; + private ScheduledDelegate debouncedModSettingsUpdate; + private void onUserModsChanged(ValueChangedEvent> mods) { + modSettingChangeTracker?.Dispose(); + if (client.Room == null) return; client.ChangeUserMods(mods.NewValue); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += onModSettingsChanged; + } + + private void onModSettingsChanged(Mod mod) + { + // Debounce changes to mod settings so as to not thrash the network. + debouncedModSettingsUpdate?.Cancel(); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => client.ChangeUserMods(UserMods.Value), 500); } private void updateBeatmapAvailability(ValueChangedEvent availability) @@ -389,10 +406,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private class UserModSelectOverlay : LocalPlayerModSelectOverlay { - public UserModSelectOverlay() - { - CustomiseButton.Alpha = 0; - } } } } From 4a405bb8598a8dcdbc6772eaac801c9fa4907956 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:04:16 +0900 Subject: [PATCH 052/102] Split ModSettingChangeTracker into separate file --- .../Configuration/ModSettingChangeTracker.cs | 39 +++++++++++++++++++ .../Configuration/SettingSourceAttribute.cs | 29 -------------- 2 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 osu.Game/Configuration/ModSettingChangeTracker.cs diff --git a/osu.Game/Configuration/ModSettingChangeTracker.cs b/osu.Game/Configuration/ModSettingChangeTracker.cs new file mode 100644 index 0000000000..f702a2fc22 --- /dev/null +++ b/osu.Game/Configuration/ModSettingChangeTracker.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Configuration +{ + public class ModSettingChangeTracker : IDisposable + { + public Action SettingChanged; + + private readonly List references = new List(); + + public ModSettingChangeTracker(IEnumerable mods) + { + foreach (var mod in mods) + { + foreach (var setting in mod.CreateSettingsControls().OfType()) + { + setting.SettingChanged += () => SettingChanged?.Invoke(mod); + references.Add(setting); + } + } + } + + public void Dispose() + { + SettingChanged = null; + + foreach (var r in references) + r.Dispose(); + references.Clear(); + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 04b8f8e962..50069be4b2 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Mods; namespace osu.Game.Configuration { @@ -141,32 +140,4 @@ namespace osu.Game.Configuration return orderedRelative.Concat(unordered); } } - - public class ModSettingChangeTracker : IDisposable - { - public Action SettingChanged; - - private readonly List references = new List(); - - public ModSettingChangeTracker(IEnumerable mods) - { - foreach (var mod in mods) - { - foreach (var setting in mod.CreateSettingsControls().OfType()) - { - setting.SettingChanged += () => SettingChanged?.Invoke(mod); - references.Add(setting); - } - } - } - - public void Dispose() - { - SettingChanged = null; - - foreach (var r in references) - r.Dispose(); - references.Clear(); - } - } } From 6fff7c39daca9c27c39511b84223e7e98172325b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:09:45 +0900 Subject: [PATCH 053/102] Ensure tracker is disposed --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 ++ osu.Game/Screens/Select/Details/AdvancedStats.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7edba3231c..59418cb348 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -402,6 +402,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client != null) client.LoadRequested -= onLoadRequested; + + modSettingChangeTracker?.Dispose(); } private class UserModSelectOverlay : LocalPlayerModSelectOverlay diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 7966ec4240..4a03c5e614 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -162,6 +162,7 @@ namespace osu.Game.Screens.Select.Details protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + settingChangeTracker?.Dispose(); starDifficultyCancellationSource?.Cancel(); } From 169acb42de35e88c8755a5140808cb3563cfb97d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:11:36 +0900 Subject: [PATCH 054/102] Xmldoc + cleanup --- .../Configuration/ModSettingChangeTracker.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/ModSettingChangeTracker.cs b/osu.Game/Configuration/ModSettingChangeTracker.cs index f702a2fc22..e2ade7dc6a 100644 --- a/osu.Game/Configuration/ModSettingChangeTracker.cs +++ b/osu.Game/Configuration/ModSettingChangeTracker.cs @@ -9,12 +9,25 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Configuration { + /// + /// A helper class for tracking changes to the settings of a set of s. + /// + /// + /// Ensure to dispose when usage is finished. + /// public class ModSettingChangeTracker : IDisposable { + /// + /// Notifies that the setting of a has changed. + /// public Action SettingChanged; - private readonly List references = new List(); + private readonly List settings = new List(); + /// + /// Creates a new for a set of s. + /// + /// The set of s whose settings need to be tracked. public ModSettingChangeTracker(IEnumerable mods) { foreach (var mod in mods) @@ -22,7 +35,7 @@ namespace osu.Game.Configuration foreach (var setting in mod.CreateSettingsControls().OfType()) { setting.SettingChanged += () => SettingChanged?.Invoke(mod); - references.Add(setting); + settings.Add(setting); } } } @@ -31,9 +44,9 @@ namespace osu.Game.Configuration { SettingChanged = null; - foreach (var r in references) + foreach (var r in settings) r.Dispose(); - references.Clear(); + settings.Clear(); } } } From 86682cdb3480e711d2ecdeab04926c7488a17046 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:16:26 +0900 Subject: [PATCH 055/102] Add client/room null check --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 59418cb348..b7adb71e2f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -336,7 +336,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { // Debounce changes to mod settings so as to not thrash the network. debouncedModSettingsUpdate?.Cancel(); - debouncedModSettingsUpdate = Scheduler.AddDelayed(() => client.ChangeUserMods(UserMods.Value), 500); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => + { + if (client.Room == null) + return; + + client.ChangeUserMods(UserMods.Value); + }, 500); } private void updateBeatmapAvailability(ValueChangedEvent availability) From c458c4cfaee1d9a830e8ddafa512d30d35b5b4cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:27:47 +0900 Subject: [PATCH 056/102] Fix unintended changes due to renaming or otherwise --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 17b0b18b52..5470d0fcb4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var osuRuleset = (DrawableOsuRuleset)drawableRuleset; - if (!ClassicNoteLock.Value) + if (ClassicNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 189ef2d76c..b1069149f3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - HitPolicy = new ObjectOrderedHitPolicy(); + HitPolicy = new StartTimeOrderedHitPolicy(); var hitWindows = new OsuHitWindows(); foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) From 321ca43b61a2a47857e04a117d622da6af385492 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:28:00 +0900 Subject: [PATCH 057/102] Update test --- .../TestSceneObjectOrderedHitPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 039a4f142f..77a68b714b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } @@ -291,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } From 4a391ce03d84adf76adb4214993c28e15cdebdbb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 21:24:41 +0900 Subject: [PATCH 058/102] Fix div-by-0 when 0 ticks are hit --- .../Objects/Drawables/DrawableSlider.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 921139c4e9..d35da64ad5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -263,16 +263,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { int totalTicks = NestedHitObjects.Count; int hitTicks = NestedHitObjects.Count(h => h.IsHit); - double hitFraction = (double)totalTicks / hitTicks; if (hitTicks == totalTicks) r.Type = HitResult.Great; - else if (hitFraction >= 0.5) - r.Type = HitResult.Ok; - else if (hitFraction > 0) - r.Type = HitResult.Meh; - else + else if (hitTicks == 0) r.Type = HitResult.Miss; + else + { + double hitFraction = (double)totalTicks / hitTicks; + + if (hitFraction >= 0.5) + r.Type = HitResult.Ok; + else if (hitFraction > 0) + r.Type = HitResult.Meh; + } }); } From 1d425b83224ff7ac765b9af6d16389e637f1d840 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 21:25:31 +0900 Subject: [PATCH 059/102] Simplify case --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index d35da64ad5..847011850c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -271,11 +271,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables else { double hitFraction = (double)totalTicks / hitTicks; - - if (hitFraction >= 0.5) - r.Type = HitResult.Ok; - else if (hitFraction > 0) - r.Type = HitResult.Meh; + r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; } }); } From bd2486e5a04bda5fb2a0ff32df6d2dce8681f2f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 21:27:12 +0900 Subject: [PATCH 060/102] Fix grammatical error in comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 847011850c..253b9800a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -258,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - // Otherwise, if this slider is also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. + // Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. ApplyResult(r => { int totalTicks = NestedHitObjects.Count; From 20a6405fd20a6cef1251eebfd7435928344679a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 22:06:19 +0900 Subject: [PATCH 061/102] Add explanatory comments + const --- .../Drawables/Connections/FollowPointConnection.cs | 6 ++++-- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 40154ca84c..5541d0e790 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -110,8 +110,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double startTime = start.GetEndTime(); double duration = end.StartTime - startTime; - // For now, adjust the pre-empt for approach rates > 10. - double preempt = PREEMPT * Math.Min(1, start.TimePreempt / 450); + // Preempt time can go below 800ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear preempt function (see: OsuHitObject). + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + double preempt = PREEMPT * Math.Min(1, start.TimePreempt / OsuHitObject.PREEMPT_MIN); fadeOutTime = startTime + fraction * duration; fadeInTime = fadeOutTime - preempt; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 6d28a576a4..22b64af3df 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// internal const float BASE_SCORING_DISTANCE = 100; + /// + /// Minimum preempt time at AR=10. + /// + public const double PREEMPT_MIN = 450; + public double TimePreempt = 600; public double TimeFadeIn = 400; @@ -113,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); - TimeFadeIn = 400 * Math.Min(1, TimePreempt / 450); + TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + + // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. + TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } From 5d1d6ec1cbeef3ff0cf17b9e04d556e01772de1a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 22:09:24 +0900 Subject: [PATCH 062/102] Fix inverted calculation --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 253b9800a6..9122f347d0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -270,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables r.Type = HitResult.Miss; else { - double hitFraction = (double)totalTicks / hitTicks; + double hitFraction = (double)hitTicks / totalTicks; r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; } }); From 07b661e28c2d2267dbe1a431f1a75e1d60fc6620 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 23:44:06 +0900 Subject: [PATCH 063/102] Add Messagepack support for serialising unknown bindable types --- .../TestAPIModMessagePackSerialization.cs | 27 +++++++++++++++++++ .../API/ModSettingsDictionaryFormatter.cs | 8 ++++++ 2 files changed, 35 insertions(+) diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs index 4294f89397..74db477cfc 100644 --- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs @@ -68,6 +68,16 @@ namespace osu.Game.Tests.Online Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); } + [Test] + public void TestDeserialiseEnumMod() + { + var apiMod = new APIMod(new TestModEnum { TestSetting = { Value = TestEnum.Value2 } }); + + var deserialized = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(apiMod)); + + Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(1)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] @@ -135,5 +145,22 @@ namespace osu.Game.Tests.Online Value = true }; } + + private class TestModEnum : Mod + { + public override string Name => "Test Mod"; + public override string Acronym => "TM"; + public override double ScoreMultiplier => 1; + + [SettingSource("Test")] + public Bindable TestSetting { get; } = new Bindable(); + } + + private enum TestEnum + { + Value1 = 0, + Value2 = 1, + Value3 = 2 + } } } diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 99e87677fa..dd854acc32 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Text; using MessagePack; using MessagePack.Formatters; @@ -41,6 +42,13 @@ namespace osu.Game.Online.API primitiveFormatter.Serialize(ref writer, b.Value, options); break; + case IBindable u: + // A mod with unknown (e.g. enum) generic type. + var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); + Debug.Assert(valueMethod != null); + primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options); + break; + default: // fall back for non-bindable cases. primitiveFormatter.Serialize(ref writer, kvp.Value, options); From 63e6ec1c9fa3b0ecb12ecbd2f389edc0042de939 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 20:06:55 +0900 Subject: [PATCH 064/102] Add audio feedback for OSD value changes --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 46 ++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 8e8a99a0a7..c5a289a85c 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -19,6 +21,15 @@ namespace osu.Game.Overlays.OSD { private const int lights_bottom_margin = 40; + private readonly int optionCount; + private readonly int selectedOption = -1; + + private SampleChannel sampleOn; + private SampleChannel sampleOff; + private SampleChannel sampleChange; + + private bool playedSample; + public TrackedSettingToast(SettingDescription description) : base(description.Name, description.Value, description.Shortcut) { @@ -46,9 +57,6 @@ namespace osu.Game.Overlays.OSD } }; - int optionCount = 0; - int selectedOption = -1; - switch (description.RawValue) { case bool val: @@ -69,6 +77,38 @@ namespace osu.Game.Overlays.OSD optionLights.Add(new OptionLight { Glowing = i == selectedOption }); } + protected override void Update() + { + base.Update(); + + if (playedSample) return; + + if (optionCount == 1) + { + if (selectedOption == 0) + sampleOn?.Play(); + else + sampleOff?.Play(); + } + else + { + if (sampleChange == null) return; + + sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f; + sampleChange.Play(); + } + + playedSample = true; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleOn = audio.Samples.Get("UI/osd-on"); + sampleOff = audio.Samples.Get("UI/osd-off"); + sampleChange = audio.Samples.Get("UI/osd-change"); + } + private class OptionLight : Container { private Color4 glowingColour, idleColour; From c12c09ec4d497ad7e52a3c4b625e3793dc23d8a1 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 11 Feb 2021 09:57:14 +0900 Subject: [PATCH 065/102] Move logic to LoadComplete instead --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index c5a289a85c..d61180baa2 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -28,8 +28,6 @@ namespace osu.Game.Overlays.OSD private SampleChannel sampleOff; private SampleChannel sampleChange; - private bool playedSample; - public TrackedSettingToast(SettingDescription description) : base(description.Name, description.Value, description.Shortcut) { @@ -77,11 +75,9 @@ namespace osu.Game.Overlays.OSD optionLights.Add(new OptionLight { Glowing = i == selectedOption }); } - protected override void Update() + protected override void LoadComplete() { - base.Update(); - - if (playedSample) return; + base.LoadComplete(); if (optionCount == 1) { @@ -97,8 +93,6 @@ namespace osu.Game.Overlays.OSD sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f; sampleChange.Play(); } - - playedSample = true; } [BackgroundDependencyLoader] From a1ae739a6235f72995ec460d93874eac2a504aa6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 15:21:50 +0900 Subject: [PATCH 066/102] Add support for Notification sounds --- .../Overlays/Notifications/Notification.cs | 24 ++++++++++++++++++- .../Notifications/NotificationSection.cs | 7 +++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 2dc6b39a92..86e409d0f6 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -40,6 +42,11 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool DisplayOnTop => true; + private SampleChannel samplePopIn; + private SampleChannel samplePopOut; + protected virtual string PopInSampleName => "UI/notification-pop-in"; + protected virtual string PopOutSampleName => "UI/overlay-pop-out"; // TODO: replace with a unique sample? + protected NotificationLight Light; private readonly CloseButton closeButton; protected Container IconContent; @@ -120,6 +127,13 @@ namespace osu.Game.Overlays.Notifications }); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + samplePopIn = audio.Samples.Get(PopInSampleName); + samplePopOut = audio.Samples.Get(PopOutSampleName); + } + protected override bool OnHover(HoverEvent e) { closeButton.FadeIn(75); @@ -143,6 +157,9 @@ namespace osu.Game.Overlays.Notifications protected override void LoadComplete() { base.LoadComplete(); + + samplePopIn?.Play(); + this.FadeInFromZero(200); NotificationContent.MoveToX(DrawSize.X); NotificationContent.MoveToX(0, 500, Easing.OutQuint); @@ -150,12 +167,17 @@ namespace osu.Game.Overlays.Notifications public bool WasClosed; - public virtual void Close() + public virtual void Close() => Close(true); + + public virtual void Close(bool playSound) { if (WasClosed) return; WasClosed = true; + if (playSound) + samplePopOut?.Play(); + Closed?.Invoke(); this.FadeOut(100); Expire(); diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index c2a958b65e..c8cee22370 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -109,7 +109,12 @@ namespace osu.Game.Overlays.Notifications private void clearAll() { - notifications.Children.ForEach(c => c.Close()); + bool playSound = true; + notifications.Children.ForEach(c => + { + c.Close(playSound); + playSound = false; + }); } protected override void Update() From 2ee634d173647a97fa9e5ef40bfe1e262920162b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 21 Jan 2021 18:42:53 +0900 Subject: [PATCH 067/102] Create subclass for "Error" notifications to allow them to have a unique pop-in sound --- .../TestSceneNotificationOverlay.cs | 20 ++++++++++++++++++- osu.Game/OsuGame.cs | 2 +- .../Notifications/SimpleErrorNotification.cs | 17 ++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/SimpleErrorNotification.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 43ba23e6c6..d0f6f3fe47 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -105,6 +105,15 @@ namespace osu.Game.Tests.Visual.UserInterface checkDisplayedCount(3); } + [Test] + public void TestError() + { + setState(Visibility.Visible); + AddStep(@"error #1", sendErrorNotification); + AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible); + checkDisplayedCount(1); + } + [Test] public void TestSpam() { @@ -179,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void sendBarrage() { - switch (RNG.Next(0, 4)) + switch (RNG.Next(0, 5)) { case 0: sendHelloNotification(); @@ -196,6 +205,10 @@ namespace osu.Game.Tests.Visual.UserInterface case 3: sendDownloadProgress(); break; + + case 4: + sendErrorNotification(); + break; } } @@ -214,6 +227,11 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.Post(new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" }); } + private void sendErrorNotification() + { + notificationOverlay.Post(new SimpleErrorNotification { Text = @"Rut roh!. Something went wrong!" }); + } + private void sendManyNotifications() { for (int i = 0; i < 10; i++) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a1f7bd233..0dc63dcd4b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -778,7 +778,7 @@ namespace osu.Game if (recentLogCount < short_term_display_limit) { - Schedule(() => notifications.Post(new SimpleNotification + Schedule(() => notifications.Post(new SimpleErrorNotification { Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs new file mode 100644 index 0000000000..13c9c5a02d --- /dev/null +++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Sprites; + +namespace osu.Game.Overlays.Notifications +{ + public class SimpleErrorNotification : SimpleNotification + { + protected override string PopInSampleName => "UI/error-notification-pop-in"; + + public SimpleErrorNotification() + { + Icon = FontAwesome.Solid.Bomb; + } + } +} From 72562070bcea0718bd6ddce220e354d934b6a987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:18:00 +0900 Subject: [PATCH 068/102] Remove second overload of Close (makes the call structure hard to follow / invoke correctly) --- osu.Game/Overlays/Notifications/Notification.cs | 6 ++---- osu.Game/Overlays/Notifications/ProgressNotification.cs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 86e409d0f6..daf931bc24 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -114,7 +114,7 @@ namespace osu.Game.Overlays.Notifications closeButton = new CloseButton { Alpha = 0, - Action = Close, + Action = () => Close(), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding @@ -167,9 +167,7 @@ namespace osu.Game.Overlays.Notifications public bool WasClosed; - public virtual void Close() => Close(true); - - public virtual void Close(bool playSound) + public virtual void Close(bool playSound = true) { if (WasClosed) return; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 3105ecd742..703c14af2b 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -150,12 +150,12 @@ namespace osu.Game.Overlays.Notifications colourCancelled = colours.Red; } - public override void Close() + public override void Close(bool playSound = true) { switch (State) { case ProgressNotificationState.Cancelled: - base.Close(); + base.Close(playSound); break; case ProgressNotificationState.Active: From 896f318b56866306e455d92c4443c252723d75f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:18:40 +0900 Subject: [PATCH 069/102] Rename variable for clarity --- osu.Game/Overlays/Notifications/NotificationSection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index c8cee22370..38ba712254 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -109,11 +109,11 @@ namespace osu.Game.Overlays.Notifications private void clearAll() { - bool playSound = true; + bool first = true; notifications.Children.ForEach(c => { - c.Close(playSound); - playSound = false; + c.Close(first); + first = false; }); } From 800f12a358b234f7d3409e843c5b913cc563f4b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:19:48 +0900 Subject: [PATCH 070/102] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7060e88026..a522a5f43d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f866b232d8..f69613cfd3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 22d104f2e1..1c602e1584 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From c8899aff92f793ee03bf61ab026263d2e6bd4dd6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 11 Feb 2021 14:36:41 +0900 Subject: [PATCH 071/102] Prevent the default on-click sample from playing for OsuCheckbox --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index f6effa0834..0d00bc0dce 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.X, }, Nub = new Nub(), - new HoverClickSounds() + new HoverSounds() }; if (nubOnRight) From f21a3c0c48abe02b8b949bd0881dfe02f5a143cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:50:55 +0900 Subject: [PATCH 072/102] Decrease the game-wide track playback volume to give samples some head-room --- osu.Game/OsuGame.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a1f7bd233..b77097a3f0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,6 +174,8 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); + [BackgroundDependencyLoader] private void load() { @@ -230,6 +232,11 @@ namespace osu.Game Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade); + // drop track volume game-wide to leave some head-room for UI effects / samples. + // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable. + // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial). + Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); + SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); } From eaa7b4cb93cdad7a27bc8958cacd860a7f2e3b10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:54:50 +0900 Subject: [PATCH 073/102] Rename second usage variable name to match --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 4a03c5e614..ab4f3f4796 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -82,15 +82,15 @@ namespace osu.Game.Screens.Select.Details mods.BindValueChanged(modsChanged, true); } - private ModSettingChangeTracker settingChangeTracker; + private ModSettingChangeTracker modSettingChangeTracker; private ScheduledDelegate debouncedStatisticsUpdate; private void modsChanged(ValueChangedEvent> mods) { - settingChangeTracker?.Dispose(); + modSettingChangeTracker?.Dispose(); - settingChangeTracker = new ModSettingChangeTracker(mods.NewValue); - settingChangeTracker.SettingChanged += m => + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += m => { if (!(m is IApplicableToDifficulty)) return; @@ -162,7 +162,7 @@ namespace osu.Game.Screens.Select.Details protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - settingChangeTracker?.Dispose(); + modSettingChangeTracker?.Dispose(); starDifficultyCancellationSource?.Cancel(); } From df7aaa5c816e6064920c32028bb3689e31174c81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 15:02:34 +0900 Subject: [PATCH 074/102] Move implementation to OsuGameBase to ensure it applies to test scenes This also removed a previous attempt at the same thing, which happened to not be applying due to the reference to the applied bindable not being held. Whoops. --- osu.Game/OsuGame.cs | 7 ------- osu.Game/OsuGameBase.cs | 9 ++++++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b77097a3f0..1a1f7bd233 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,8 +174,6 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); - [BackgroundDependencyLoader] private void load() { @@ -232,11 +230,6 @@ namespace osu.Game Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade); - // drop track volume game-wide to leave some head-room for UI effects / samples. - // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable. - // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial). - Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); - SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d3936ed27e..a1b66ba9c0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -155,6 +155,8 @@ namespace osu.Game protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); + [BackgroundDependencyLoader] private void load() { @@ -278,9 +280,10 @@ namespace osu.Game RegisterImportHandler(ScoreManager); RegisterImportHandler(SkinManager); - // tracks play so loud our samples can't keep up. - // this adds a global reduction of track volume for the time being. - Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); + // drop track volume game-wide to leave some head-room for UI effects / samples. + // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable. + // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial). + Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); Beatmap = new NonNullableBindable(defaultBeatmap); From dddd776802ad1f77f085c9862c317f93a3d1b191 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 16:38:17 +0900 Subject: [PATCH 075/102] Add the ability for settings items to have tooltips --- osu.Game/Configuration/SettingSourceAttribute.cs | 6 ++++++ osu.Game/Overlays/Settings/SettingsItem.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 50069be4b2..70d67aaaa0 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -57,6 +57,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, + TooltipText = attr.Description, Current = bNumber, KeyboardStep = 0.1f, }; @@ -67,6 +68,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, + TooltipText = attr.Description, Current = bNumber, KeyboardStep = 0.1f, }; @@ -77,6 +79,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, + TooltipText = attr.Description, Current = bNumber }; @@ -86,6 +89,7 @@ namespace osu.Game.Configuration yield return new SettingsCheckbox { LabelText = attr.Label, + TooltipText = attr.Description, Current = bBool }; @@ -95,6 +99,7 @@ namespace osu.Game.Configuration yield return new SettingsTextBox { LabelText = attr.Label, + TooltipText = attr.Description, Current = bString }; @@ -105,6 +110,7 @@ namespace osu.Game.Configuration var dropdown = (Drawable)Activator.CreateInstance(dropdownType); dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); + dropdownType.GetProperty(nameof(SettingsDropdown.TooltipText))?.SetValue(dropdown, attr.Description); dropdownType.GetProperty(nameof(SettingsDropdown.Current))?.SetValue(dropdown, bindable); yield return dropdown; diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 278479e04f..27232d0a49 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -21,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue + public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue, IHasTooltip { protected abstract Drawable CreateControl(); @@ -214,5 +214,7 @@ namespace osu.Game.Overlays.Settings this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); } } + + public string TooltipText { get; set; } } } From 9fb41dc0b6f416e2232cffbeb576f0d9cdcf5955 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 16:41:21 +0900 Subject: [PATCH 076/102] Move property to a better place in the class --- osu.Game/Overlays/Settings/SettingsItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 27232d0a49..af225889da 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -37,6 +37,8 @@ namespace osu.Game.Overlays.Settings public bool ShowsDefaultIndicator = true; + public string TooltipText { get; set; } + public virtual string LabelText { get => labelText?.Text ?? string.Empty; @@ -214,7 +216,5 @@ namespace osu.Game.Overlays.Settings this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); } } - - public string TooltipText { get; set; } } } From db79080bc4deebef8277866157d3067b024ee533 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:14:49 +0900 Subject: [PATCH 077/102] Fix GetNodeSamples potentially returning a live reference and overwriting existing samples --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1670df24a8..6fb36e80bc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -140,7 +140,8 @@ namespace osu.Game.Rulesets.Osu.Objects // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. // For now, the samples are attached to and played by the slider itself at the correct end time. - Samples = this.GetNodeSamples(repeatCount + 1); + // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live). + Samples = this.GetNodeSamples(repeatCount + 1).ToArray(); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From e9730d4782e9e8716d61bcf3b7e91c5cb2d1573d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:16:17 +0900 Subject: [PATCH 078/102] Move default sample addition to inside PlacementBlueprint This isn't actually required to fix the behaviour but it does feel like a better place to put this logic. --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 4 ++++ .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index c0eb891f5e..bfff93e7c5 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; @@ -45,6 +46,9 @@ namespace osu.Game.Rulesets.Edit { HitObject = hitObject; + // adding the default hit sample should be the case regardless of the ruleset. + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); + RelativeSizeAxes = Axes.Both; // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index c09b935f28..79f457c050 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -211,9 +211,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { - // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. - blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); - placementBlueprintContainer.Child = currentPlacement = blueprint; // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame From f84ea3063768bac3d8850c62f187f681ece7bbba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:47:29 +0900 Subject: [PATCH 079/102] Expose Mods in DrawableRuleset to avoid using external DI --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 +-- osu.Game/Rulesets/Mods/ModAutoplay.cs | 3 +-- osu.Game/Rulesets/Mods/ModCinema.cs | 4 +--- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 ++-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 59a5295858..77de0cb45b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -62,8 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods inputManager.AllowUserCursorMovement = false; // Generate the replay frames the cursor should follow - var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); - replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, mods).Generate().Frames.Cast().ToList(); + replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList(); } } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 748c7272f4..d1d23def67 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -18,8 +18,7 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); } } diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 16e6400f23..eb0473016a 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -15,8 +14,7 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); // AlwaysPresent required for hitsounds drawableRuleset.Playfield.AlwaysPresent = true; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 6940e43e5b..ca27e6b21a 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.UI protected IRulesetConfigManager Config { get; private set; } [Cached(typeof(IReadOnlyList))] - protected override IReadOnlyList Mods { get; } + public sealed override IReadOnlyList Mods { get; } private FrameStabilityContainer frameStabilityContainer; @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.UI /// /// The mods which are to be applied. /// - protected abstract IReadOnlyList Mods { get; } + public abstract IReadOnlyList Mods { get; } /// ~ /// The associated ruleset. From ffd3caacb5167bf10d49587ef15cde22ae5896f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:57:50 +0900 Subject: [PATCH 080/102] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index a522a5f43d..d88a11257d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f69613cfd3..d68a8a515c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 1c602e1584..87ebd41fee 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 970039b7e3ff29804999cc0f28633bb308db0eee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:14:49 +0900 Subject: [PATCH 081/102] Split out hover sample debounce logic so it can be more easily used in other places --- .../HoverSampleDebounceComponent.cs | 46 +++++++++++++++++++ .../Graphics/UserInterface/HoverSounds.cs | 32 ++----------- 2 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs diff --git a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs new file mode 100644 index 0000000000..f0c7c20fe8 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Configuration; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Handles debouncing hover sounds at a global level to ensure the effects are not overwhelming. + /// + public abstract class HoverSampleDebounceComponent : CompositeDrawable + { + /// + /// Length of debounce for hover sound playback, in milliseconds. + /// + public double HoverDebounceTime { get; } = 20; + + private Bindable lastPlaybackTime; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, SessionStatics statics) + { + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); + } + + protected override bool OnHover(HoverEvent e) + { + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; + + if (enoughTimePassedSinceLastPlayback) + { + PlayHoverSample(); + lastPlaybackTime.Value = Time.Current; + } + + return false; + } + + public abstract void PlayHoverSample(); + } +} diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index fa43d4543f..29238377c7 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -5,11 +5,8 @@ using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Framework.Utils; @@ -19,19 +16,12 @@ namespace osu.Game.Graphics.UserInterface /// Adds hover sounds to a drawable. /// Does not draw anything. /// - public class HoverSounds : CompositeDrawable + public class HoverSounds : HoverSampleDebounceComponent { private SampleChannel sampleHover; - /// - /// Length of debounce for hover sound playback, in milliseconds. - /// - public double HoverDebounceTime { get; } = 20; - protected readonly HoverSampleSet SampleSet; - private Bindable lastPlaybackTime; - public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) { SampleSet = sampleSet; @@ -41,27 +31,13 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, SessionStatics statics) { - lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); - sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); } - protected override bool OnHover(HoverEvent e) + public override void PlayHoverSample() { - if (sampleHover == null) - return false; - - bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - - if (enoughTimePassedSinceLastPlayback) - { - sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); - sampleHover.Play(); - - lastPlaybackTime.Value = Time.Current; - } - - return false; + sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); + sampleHover.Play(); } } From cd01591dda9ae57e66eddb0d5a80564a98f7ab20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:14:57 +0900 Subject: [PATCH 082/102] Consume new debounce logic in carousel header --- .../Screens/Select/Carousel/CarouselHeader.cs | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 4f53a6e202..90eebfc05b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -20,10 +21,6 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { - private SampleChannel sampleHover; - - private readonly Box hoverLayer; - public Container BorderContainer; public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); @@ -44,23 +41,11 @@ namespace osu.Game.Screens.Select.Carousel Children = new Drawable[] { Content, - hoverLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Blending = BlendingParameters.Additive, - }, + new HoverLayer() } }; } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) - { - sampleHover = audio.Samples.Get("SongSelect/song-ping"); - hoverLayer.Colour = colours.Blue.Opacity(0.1f); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -97,22 +82,50 @@ namespace osu.Game.Screens.Select.Carousel } } - protected override bool OnHover(HoverEvent e) + public class HoverLayer : HoverSampleDebounceComponent { - if (sampleHover != null) + private SampleChannel sampleHover; + + private Box box; + + public HoverLayer() { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + InternalChild = box = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }; + + sampleHover = audio.Samples.Get("SongSelect/song-ping"); + } + + protected override bool OnHover(HoverEvent e) + { + box.FadeIn(100, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + box.FadeOut(1000, Easing.OutQuint); + base.OnHoverLost(e); + } + + public override void PlayHoverSample() + { + if (sampleHover == null) return; + sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2); sampleHover.Play(); } - - hoverLayer.FadeIn(100, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hoverLayer.FadeOut(1000, Easing.OutQuint); - base.OnHoverLost(e); } } } From a2035a2e84d6a187331e0c56e2ffa906be673659 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:20:39 +0900 Subject: [PATCH 083/102] Stop hover sounds from playing when dragging (scrolling) --- .../Graphics/UserInterface/HoverSampleDebounceComponent.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs index f0c7c20fe8..55f43cfe46 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs @@ -30,6 +30,10 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { + // hover sounds shouldn't be played during scroll operations. + if (e.HasAnyButtonPressed) + return false; + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; if (enoughTimePassedSinceLastPlayback) From 5f23bd725941a94304481d7aa37409162426e8ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:48:32 +0900 Subject: [PATCH 084/102] Revert most of the changes to ArchiveModeManager by using better code --- osu.Game/Beatmaps/BeatmapManager.cs | 8 ++------ osu.Game/Database/ArchiveModelManager.cs | 26 ++++++++++++++---------- osu.Game/Scoring/ScoreManager.cs | 7 +++---- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4825569ee4..f23e135c68 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -64,13 +64,9 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); + protected override string ImportFromStablePath => "."; - protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) - { - var songStorage = stableStorage.GetSongStorage(); - return songStorage.GetDirectories(".").Select(path => songStorage.GetFullPath(path)); - } + protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index fd94660a4b..b55020c437 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -637,16 +637,11 @@ namespace osu.Game.Database /// protected virtual string ImportFromStablePath => null; - /// - /// Checks for the existence of an osu-stable directory. - /// - protected virtual bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); - /// /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) - .Select(path => stableStorage.GetFullPath(path)); + protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath) + .Select(path => storage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. @@ -660,24 +655,33 @@ namespace osu.Game.Database /// public Task ImportFromStableAsync() { - var stable = GetStableStorage?.Invoke(); + var stableStorage = GetStableStorage?.Invoke(); - if (stable == null) + if (stableStorage == null) { Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } - if (!StableDirectoryExists(stable)) + var storage = PrepareStableStorage(stableStorage); + + if (!storage.ExistsDirectory(ImportFromStablePath)) { // This handles situations like when the user does not have a Skins folder Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } - return Task.Run(async () => await Import(GetStableImportPaths(stable).ToArray())); + return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray())); } + /// + /// Run any required traversal operations on the stable storage location before performing operations. + /// + /// The stable storage. + /// The usable storage. Return the unchanged if no traversal is required. + protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage; + #endregion /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 6aa0a30a75..a6beb19876 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -16,7 +16,6 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -72,9 +71,9 @@ namespace osu.Game.Scoring } } - protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) - => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => stableStorage.GetFullPath(path)); + protected override IEnumerable GetStableImportPaths(Storage storage) + => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); From 8ab7d07eab83befc7332496d20250fe54bf5ddc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:57:57 +0900 Subject: [PATCH 085/102] Tidy up config parsing logic --- osu.Game/IO/StableStorage.cs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index ccc6f9c311..d4b0d300ff 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -23,6 +23,7 @@ namespace osu.Game.IO : base(path, host) { this.host = host; + songsPath = new Lazy(locateSongsDirectory); } @@ -33,32 +34,29 @@ namespace osu.Game.IO private string locateSongsDirectory() { - var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault(); - if (configFile == null) - return songsDirectoryPath; - - using (var stream = GetStream(configFile)) - using (var textReader = new StreamReader(stream)) + if (configFile != null) { - string line; - - while ((line = textReader.ReadLine()) != null) + using (var stream = GetStream(configFile)) + using (var textReader = new StreamReader(stream)) { - if (line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) + string line; + + while ((line = textReader.ReadLine()) != null) { - var directory = line.Split('=')[1].TrimStart(); - if (Path.IsPathFullyQualified(directory)) - songsDirectoryPath = directory; + if (!line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) continue; + + var customDirectory = line.Split('=').LastOrDefault()?.Trim(); + if (customDirectory != null && Path.IsPathFullyQualified(customDirectory)) + return customDirectory; break; } } } - return songsDirectoryPath; + return GetFullPath(stable_default_songs_path); } } } From 33c9ecac8a291657d19e4d4ba3cf7c55e15e3a5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 14:54:19 +0900 Subject: [PATCH 086/102] Fix MessageFormatter not working for custom endpoints --- osu.Game/Online/Chat/MessageFormatter.cs | 7 ++++++- osu.Game/OsuGameBase.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d2a117876d..8673d73be7 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -49,6 +49,11 @@ namespace osu.Game.Online.Chat // Unicode emojis private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])"); + /// + /// The root URL for the website, used for chat link matching. + /// + public static string WebsiteRootUrl { get; set; } = "https://osu.ppy.sh"; + private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { int captureOffset = 0; @@ -119,7 +124,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && args[1] == "osu.ppy.sh") + if (args.Length > 3 && url.StartsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase)) { switch (args[2]) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a1b66ba9c0..174b5006a2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -31,6 +31,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online; +using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; @@ -225,6 +226,8 @@ namespace osu.Game EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; + dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); From 6a42d312f62fd1ba45b2ffda559fbcb31b075c74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 14:56:46 +0900 Subject: [PATCH 087/102] Match using EndsWith to ignore protocol (and allow http) --- osu.Game/Online/Chat/MessageFormatter.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 8673d73be7..d8e02e5b6d 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -52,7 +52,14 @@ namespace osu.Game.Online.Chat /// /// The root URL for the website, used for chat link matching. /// - public static string WebsiteRootUrl { get; set; } = "https://osu.ppy.sh"; + public static string WebsiteRootUrl + { + set => websiteRootUrl = value + .Trim('/') // trim potential trailing slash/ + .Split('/').Last(); // only keep domain name, ignoring protocol. + } + + private static string websiteRootUrl; private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { @@ -124,7 +131,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && url.StartsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase)) + if (args.Length > 3 && args[1].EndsWith(websiteRootUrl, StringComparison.OrdinalIgnoreCase)) { switch (args[2]) { From 1c5aaf3832e97a258bb764aa5dfd2f07200e4b35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:03:53 +0900 Subject: [PATCH 088/102] Add back default value --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d8e02e5b6d..3c6df31462 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Chat .Split('/').Last(); // only keep domain name, ignoring protocol. } - private static string websiteRootUrl; + private static string websiteRootUrl = "osu.ppy.sh"; private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { From 955c9a2dd3e47dab3752d62f4173bbde3b5ec0d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:17:39 +0900 Subject: [PATCH 089/102] Add test coverage of beatmap link resolution --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 600c820ce1..151e05b18d 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -21,6 +21,21 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(36, result.Links[0].Length); } + [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")] + public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) + { + Message result = MessageFormatter.FormatMessage(new Message { Content = link }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual(expectedAction, result.Links[0].Action); + Assert.AreEqual(expectedArg, result.Links[0].Argument); + } + [Test] public void TestMultipleComplexLinks() { From bb9123eecd231cde3b1ed240e7ffe3575b005fee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:17:54 +0900 Subject: [PATCH 090/102] Better handle fallback scenarios for beatmap links --- osu.Game/Online/Chat/MessageFormatter.cs | 33 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d2a117876d..5aab476b6d 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -121,20 +121,37 @@ namespace osu.Game.Online.Chat // length > 3 since all these links need another argument to work if (args.Length > 3 && args[1] == "osu.ppy.sh") { + var mainArg = args[3]; + switch (args[2]) { + // old site only case "b": case "beatmaps": - return new LinkDetails(LinkAction.OpenBeatmap, args[3]); + { + string trimmed = mainArg.Split('?').First(); + if (int.TryParse(trimmed, out var id)) + return new LinkDetails(LinkAction.OpenBeatmap, id.ToString()); + + break; + } case "s": case "beatmapsets": case "d": - return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); + { + if (args.Length > 4 && int.TryParse(args[4], out var id)) + // https://osu.ppy.sh/beatmapsets/1154158#osu/2768184 + return new LinkDetails(LinkAction.OpenBeatmap, id.ToString()); + + // https://osu.ppy.sh/beatmapsets/1154158#whatever + string trimmed = mainArg.Split('#').First(); + return new LinkDetails(LinkAction.OpenBeatmapSet, trimmed); + } case "u": case "users": - return new LinkDetails(LinkAction.OpenUserProfile, args[3]); + return new LinkDetails(LinkAction.OpenUserProfile, mainArg); } } @@ -183,10 +200,9 @@ namespace osu.Game.Online.Chat case "osump": return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]); - - default: - return new LinkDetails(LinkAction.External, null); } + + return new LinkDetails(LinkAction.External, null); } private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) @@ -259,8 +275,9 @@ namespace osu.Game.Online.Chat public class LinkDetails { - public LinkAction Action; - public string Argument; + public readonly LinkAction Action; + + public readonly string Argument; public LinkDetails(LinkAction action, string argument) { From 3799493536907954559ed707d2edf07a1bd02c26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:25:00 +0900 Subject: [PATCH 091/102] Add test coverage of int match failures --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 4 ++++ osu.Game/Online/Chat/MessageFormatter.cs | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 151e05b18d..11e94f0b89 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -24,8 +24,10 @@ namespace osu.Game.Tests.Chat [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")] [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")] [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")] + [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc/def")] [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")] [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")] + [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc")] public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) { Message result = MessageFormatter.FormatMessage(new Message { Content = link }); @@ -34,6 +36,8 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(1, result.Links.Count); Assert.AreEqual(expectedAction, result.Links[0].Action); Assert.AreEqual(expectedArg, result.Links[0].Argument); + if (expectedAction == LinkAction.External) + Assert.AreEqual(link, result.Links[0].Url); } [Test] diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 5aab476b6d..8e92078c2c 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -146,7 +146,10 @@ namespace osu.Game.Online.Chat // https://osu.ppy.sh/beatmapsets/1154158#whatever string trimmed = mainArg.Split('#').First(); - return new LinkDetails(LinkAction.OpenBeatmapSet, trimmed); + if (int.TryParse(trimmed, out id)) + return new LinkDetails(LinkAction.OpenBeatmapSet, id.ToString()); + + break; } case "u": From a1be3c8bfdf71df5b0b0476b379d36a427ee0ee4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Feb 2021 15:27:37 +0900 Subject: [PATCH 092/102] Fix header background being invisible in multiplayer/playlists --- .../Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index eb05cbaf85..3206f7b3ab 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -34,8 +34,8 @@ namespace osu.Game.Beatmaps.Drawables /// protected virtual double UnloadDelay => 10000; - protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay); + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) => + new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay) { RelativeSizeAxes = Axes.Both }; protected override double TransformDuration => 400; From f7374703f00ee87f8865a51c5ef2fa5c3befad79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:29:21 +0900 Subject: [PATCH 093/102] Update tests to match dev domain --- .../Visual/Online/TestSceneChatLink.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 9e69530a77..74f53ebdca 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -103,26 +103,26 @@ namespace osu.Game.Tests.Visual.Online private void testLinksGeneral() { addMessageWithChecks("test!"); - addMessageWithChecks("osu.ppy.sh!"); - addMessageWithChecks("https://osu.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("dev.ppy.sh!"); + addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External); addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp); addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("(osu forums)[https://osu.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://osu.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[osu forums](https://osu.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://osu.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("is now listening to [https://osu.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); - addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); - addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3, + addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); + addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); + addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3, expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); - addMessageWithChecks("[https://osu.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://osu.ppy.sh/home)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://osu.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External }); + addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External }); // note that there's 0 links here (they get removed if a channel is not found) addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); addMessageWithChecks("I am important!", 0, false, true); addMessageWithChecks("feels important", 0, true, true); - addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); + addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel); @@ -136,9 +136,9 @@ namespace osu.Game.Tests.Visual.Online int echoCounter = 0; addEchoWithWait("sent!", "received!"); - addEchoWithWait("https://osu.ppy.sh/home", null, 500); - addEchoWithWait("[https://osu.ppy.sh/forum let's try multiple words too!]"); - addEchoWithWait("(long loading times! clickable while loading?)[https://osu.ppy.sh/home]", null, 5000); + addEchoWithWait("https://dev.ppy.sh/home", null, 500); + addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]"); + addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000); void addEchoWithWait(string text, string completeText = null, double delay = 250) { From a0733769206b58026ec0b044da3e2820b71a0c76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:18:16 +0900 Subject: [PATCH 094/102] Show URLs in tooltips when custom text has replaced the link --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index e3a9a5fe9d..914c8ff78d 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -38,7 +38,12 @@ namespace osu.Game.Graphics.Containers foreach (var link in links) { AddText(text[previousLinkEnd..link.Index]); - AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url); + + string displayText = text.Substring(link.Index, link.Length); + string linkArgument = link.Argument ?? link.Url; + string tooltip = displayText == link.Url ? null : link.Url; + + AddLink(displayText, link.Action, linkArgument, tooltip); previousLinkEnd = link.Index + link.Length; } @@ -52,7 +57,7 @@ namespace osu.Game.Graphics.Containers => createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action); public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action creationParameters = null) - => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null); + => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), tooltipText); public void AddLink(IEnumerable text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null) { From 4ab16694d1ca145d2af3f6aa758504e9dba0a511 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 16:22:19 +0900 Subject: [PATCH 095/102] Fix classic "welcome" intro not looping as expected --- osu.Game/Screens/Menu/IntroWelcome.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index abb83f894a..d454d85d9e 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -67,6 +67,10 @@ namespace osu.Game.Screens.Menu { StartTrack(); + // this classic intro loops forever. + if (UsingThemedIntro) + Track.Looping = true; + const float fade_in_time = 200; logo.ScaleTo(1); From 725db5683731e9b193fb9d309910c88e8e6ba398 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 16:55:34 +0900 Subject: [PATCH 096/102] Add loading spinner while tournament bracket is loading / retrieving data --- .../TournamentTestBrowser.cs | 2 + .../TournamentTestScene.cs | 2 + osu.Game.Tournament/TournamentGame.cs | 43 +++++++++++++------ osu.Game.Tournament/TournamentGameBase.cs | 22 ++++++---- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index f7ad757926..2f50ae4141 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -13,6 +13,8 @@ namespace osu.Game.Tournament.Tests { base.LoadComplete(); + BracketLoadTask.Wait(); + LoadComponentAsync(new Background("Menu/menu-background-0") { Colour = OsuColour.Gray(0.5f), diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index d22da25f9d..62882d7188 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -154,6 +154,8 @@ namespace osu.Game.Tournament.Tests protected override void LoadAsyncComplete() { + BracketLoadTask.Wait(); + // this has to be run here rather than LoadComplete because // TestScene.cs is checking the IsLoaded state (on another thread) and expects // the runner to be loaded at that point. diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index bbe4a53d8f..fadb821bef 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Colour; using osu.Game.Graphics.Cursor; using osu.Game.Tournament.Models; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -32,25 +33,24 @@ namespace osu.Game.Tournament private Drawable heightWarning; private Bindable windowSize; private Bindable windowMode; + private LoadingSpinner loadingSpinner; [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) { windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); - windowSize.BindValueChanged(size => ScheduleAfterChildren(() => - { - var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; - - heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; - }), true); - windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode); - windowMode.BindValueChanged(mode => ScheduleAfterChildren(() => - { - windowMode.Value = WindowMode.Windowed; - }), true); - AddRange(new[] + Add(loadingSpinner = new LoadingSpinner(true, true) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(40), + }); + + loadingSpinner.Show(); + + BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[] { new Container { @@ -93,7 +93,24 @@ namespace osu.Game.Tournament RelativeSizeAxes = Axes.Both, Child = new TournamentSceneManager() } - }); + }, drawables => + { + loadingSpinner.Hide(); + loadingSpinner.Expire(); + + AddRange(drawables); + + windowSize.BindValueChanged(size => ScheduleAfterChildren(() => + { + var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; + heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; + }), true); + + windowMode.BindValueChanged(mode => ScheduleAfterChildren(() => + { + windowMode.Value = WindowMode.Windowed; + }), true); + })); } } } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 97c950261b..4dd072cf17 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; @@ -29,6 +31,8 @@ namespace osu.Game.Tournament private DependencyContainer dependencies; private FileBasedIPC ipc; + protected Task BracketLoadTask { get; private set; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -46,14 +50,9 @@ namespace osu.Game.Tournament Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); - readBracket(); - - ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + BracketLoadTask = Task.Run(readBracket); dependencies.CacheAs(new StableInfo(storage)); - - dependencies.CacheAs(ipc = new FileBasedIPC()); - Add(ipc); } private void readBracket() @@ -70,8 +69,6 @@ namespace osu.Game.Tournament Ruleset.BindTo(ladder.Ruleset); - dependencies.Cache(ladder); - bool addedInfo = false; // assign teams @@ -127,6 +124,15 @@ namespace osu.Game.Tournament if (addedInfo) SaveChanges(); + + ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + + Schedule(() => + { + dependencies.Cache(ladder); + dependencies.CacheAs(ipc = new FileBasedIPC()); + Add(ipc); + }); } /// From 37a21cb192760c5f68a7842140d59d9421d681ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 21:30:02 +0900 Subject: [PATCH 097/102] Set static locally in test to ensure tests always run correctly --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 11e94f0b89..b80da928c8 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -21,15 +21,17 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(36, result.Links[0].Length); } - [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")] - [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")] - [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")] - [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc/def")] - [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")] - [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")] - [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456?whatever")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123/456")] + [TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc/def")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")] + [TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc")] public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) { + MessageFormatter.WebsiteRootUrl = "dev.ppy.sh"; + Message result = MessageFormatter.FormatMessage(new Message { Content = link }); Assert.AreEqual(result.Content, result.DisplayContent); From 7d057ab6ce9d81a6afe6fe79884d7705a40491dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 22:38:55 +0900 Subject: [PATCH 098/102] Fix two threading issues --- osu.Game.Tournament/TournamentGameBase.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 4dd072cf17..4224da4bbe 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; @@ -31,7 +30,9 @@ namespace osu.Game.Tournament private DependencyContainer dependencies; private FileBasedIPC ipc; - protected Task BracketLoadTask { get; private set; } + protected Task BracketLoadTask => taskCompletionSource.Task; + + private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -50,9 +51,9 @@ namespace osu.Game.Tournament Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); - BracketLoadTask = Task.Run(readBracket); - dependencies.CacheAs(new StableInfo(storage)); + + Task.Run(readBracket); } private void readBracket() @@ -67,8 +68,6 @@ namespace osu.Game.Tournament ladder ??= new LadderInfo(); ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First(); - Ruleset.BindTo(ladder.Ruleset); - bool addedInfo = false; // assign teams @@ -129,9 +128,13 @@ namespace osu.Game.Tournament Schedule(() => { + Ruleset.BindTo(ladder.Ruleset); + dependencies.Cache(ladder); dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); + + taskCompletionSource.SetResult(true); }); } From 13aaf766f93eadf358ea9d6b0d0bac2e71358a46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Feb 2021 01:10:39 +0900 Subject: [PATCH 099/102] Fix regression in tournament test startup behaviour --- .../TournamentTestBrowser.cs | 19 ++++++++++--------- .../TournamentTestScene.cs | 13 +++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index 2f50ae4141..50bdcd86c5 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -13,17 +13,18 @@ namespace osu.Game.Tournament.Tests { base.LoadComplete(); - BracketLoadTask.Wait(); - - LoadComponentAsync(new Background("Menu/menu-background-0") + BracketLoadTask.ContinueWith(_ => Schedule(() => { - Colour = OsuColour.Gray(0.5f), - Depth = 10 - }, AddInternal); + LoadComponentAsync(new Background("Menu/menu-background-0") + { + Colour = OsuColour.Gray(0.5f), + Depth = 10 + }, AddInternal); - // Have to construct this here, rather than in the constructor, because - // we depend on some dependencies to be loaded within OsuGameBase.load(). - Add(new TestBrowser()); + // Have to construct this here, rather than in the constructor, because + // we depend on some dependencies to be loaded within OsuGameBase.load(). + Add(new TestBrowser()); + })); } } } diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index 62882d7188..025abfcbc6 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -154,12 +154,13 @@ namespace osu.Game.Tournament.Tests protected override void LoadAsyncComplete() { - BracketLoadTask.Wait(); - - // this has to be run here rather than LoadComplete because - // TestScene.cs is checking the IsLoaded state (on another thread) and expects - // the runner to be loaded at that point. - Add(runner = new TestSceneTestRunner.TestRunner()); + BracketLoadTask.ContinueWith(_ => Schedule(() => + { + // this has to be run here rather than LoadComplete because + // TestScene.cs is checking the IsLoaded state (on another thread) and expects + // the runner to be loaded at that point. + Add(runner = new TestSceneTestRunner.TestRunner()); + })); } public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); From 52975c51854c665495a8384d5356829e90062bfc Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 12 Feb 2021 10:23:33 -0800 Subject: [PATCH 100/102] Remove hardcoded padding from main content --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b7adb71e2f..56882e0d38 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Horizontal = 105, + Horizontal = HORIZONTAL_OVERFLOW_PADDING + 55, Vertical = 20 }, Child = new GridContainer From b28a906197eee66455c1201db6e3288afca2f571 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 12 Feb 2021 10:29:29 -0800 Subject: [PATCH 101/102] Fix extra mod settings overflowing from screen --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 56882e0d38..c5130baa94 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -237,6 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, Height = 0.5f, + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }, Child = userModsSelectOverlay = new UserModSelectOverlay { SelectedMods = { BindTarget = UserMods }, From 982d8e35edc3efb9a05111b37596c94c44e182e8 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 12 Feb 2021 10:42:48 -0800 Subject: [PATCH 102/102] Fix mod settings showing scrollbar when screen is offset --- osu.Game/Overlays/Mods/ModSettingsContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs index 1c57ff54ad..64d65cab3b 100644 --- a/osu.Game/Overlays/Mods/ModSettingsContainer.cs +++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs @@ -52,6 +52,7 @@ namespace osu.Game.Overlays.Mods new OsuScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, Child = modSettingsContent = new FillFlowContainer { Anchor = Anchor.TopCentre,