diff --git a/.editorconfig b/.editorconfig index a5f7795882..0cdf3b92d3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -194,4 +194,7 @@ dotnet_diagnostic.IDE0068.severity = none dotnet_diagnostic.IDE0069.severity = none #Disable operator overloads requiring alternate named methods -dotnet_diagnostic.CA2225.severity = none \ No newline at end of file +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md index 0b80ce44dd..e45893b97a 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-issues.md +++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md @@ -1,7 +1,18 @@ --- name: Bug Report -about: Issues regarding encountered bugs. +about: Report a bug or crash to desktop --- + +<!-- +IMPORTANT: Your issue may already be reported. + +Please check: +- Pinned issues, at the top of https://github.com/ppy/osu/issues +- Current priority 0 issues at https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0 +- Search for your issue. If you find that it already exists, please respond with a reaction or add any further information that may be helpful. +--> + + **Describe the bug:** **Screenshots or videos showing encountered issue:** @@ -9,8 +20,11 @@ about: Issues regarding encountered bugs. **osu!lazer version:** **Logs:** + <!-- *please attach logs here, which are located at:* - `%AppData%/osu/logs` *(on Windows),* - `~/.local/share/osu/logs` *(on Linux & macOS).* +- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*, +- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer) --> diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md deleted file mode 100644 index ada8de73c0..0000000000 --- a/.github/ISSUE_TEMPLATE/02-crash-issues.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Crash Report -about: Issues regarding crashes or permanent freezes. ---- -**Describe the crash:** - -**Screenshots or videos showing encountered issue:** - -**osu!lazer version:** - -**Logs:** -<!-- -*please attach logs here, which are located at:* -- `%AppData%/osu/logs` *(on Windows),* -- `~/.local/share/osu/logs` *(on Linux & macOS).* ---> - -**Computer Specifications:** diff --git a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md similarity index 62% rename from .github/ISSUE_TEMPLATE/03-feature-request-issues.md rename to .github/ISSUE_TEMPLATE/02-feature-request-issues.md index 54c4ff94e5..c3357dd780 100644 --- a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md +++ b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md @@ -1,6 +1,6 @@ --- name: Feature Request -about: Features you would like to see in the game! +about: Propose a feature you would like to see in the game! --- **Describe the new feature:** diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml deleted file mode 100644 index 9ece926b34..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml +++ /dev/null @@ -1,20 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="osu! (legacy osuTK)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false"> - <option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0/osu!.dll" /> - <option name="PROGRAM_PARAMETERS" value="--tk" /> - <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0" /> - <option name="PASS_PARENT_ENVS" value="1" /> - <option name="USE_EXTERNAL_CONSOLE" value="0" /> - <option name="USE_MONO" value="0" /> - <option name="RUNTIME_ARGUMENTS" value="" /> - <option name="PROJECT_PATH" value="$PROJECT_DIR$/osu.Desktop/osu.Desktop.csproj" /> - <option name="PROJECT_EXE_PATH_TRACKING" value="1" /> - <option name="PROJECT_ARGUMENTS_TRACKING" value="1" /> - <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" /> - <option name="PROJECT_KIND" value="DotNetCore" /> - <option name="PROJECT_TFM" value="net5.0" /> - <method v="2"> - <option name="Build" /> - </method> - </configuration> -</component> \ No newline at end of file diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 47839608c9..46c50dbfa2 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -7,3 +7,4 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. +M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead. \ No newline at end of file diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index d497365f87..6a99e230d1 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -30,7 +30,7 @@ <Rule Id="CA1819" Action="None" /> <Rule Id="CA1822" Action="None" /> <Rule Id="CA1823" Action="None" /> - <Rule Id="CA2007" Action="None" /> + <Rule Id="CA2007" Action="Warning" /> <Rule Id="CA2214" Action="None" /> <Rule Id="CA2227" Action="None" /> </Rules> diff --git a/Directory.Build.props b/Directory.Build.props index 2e1873a9ed..53ad973e47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ <ItemGroup Label="Code Analysis"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.2" PrivateAssets="All" /> + <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" /> </ItemGroup> <PropertyGroup Label="Code Analysis"> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet> diff --git a/osu.Android.props b/osu.Android.props index 1513f6444d..3682a44b9f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ </ItemGroup> <ItemGroup> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" /> - <PackageReference Include="ppy.osu.Framework.Android" Version="2021.220.0" /> + <PackageReference Include="ppy.osu.Framework.Android" Version="2021.329.0" /> </ItemGroup> </Project> diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index ad929bbac3..cffcea22c2 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -17,7 +17,7 @@ using osu.Game.Database; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] @@ -100,15 +100,15 @@ namespace osu.Android // copy to an arbitrary-access memory stream to be able to proceed with the import. var copy = new MemoryStream(); using (var stream = ContentResolver.OpenInputStream(uri)) - await stream.CopyToAsync(copy); + await stream.CopyToAsync(copy).ConfigureAwait(false); lock (tasks) { tasks.Add(new ImportTask(copy, filename)); } - })); + })).ConfigureAwait(false); - await game.Import(tasks.ToArray()); + await game.Import(tasks.ToArray()).ConfigureAwait(false); }, TaskCreationOptions.LongRunning); } } diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj index a2638e95c8..2051beae21 100644 --- a/osu.Android/osu.Android.csproj +++ b/osu.Android/osu.Android.csproj @@ -53,5 +53,10 @@ <ItemGroup> <AndroidResource Include="Resources\drawable\lazer.png" /> </ItemGroup> + <ItemGroup> + <PackageReference Include="System.Formats.Asn1"> + <Version>5.0.0</Version> + </PackageReference> + </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> </Project> \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 5909b82c8f..b2487568ce 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -136,24 +136,12 @@ namespace osu.Desktop var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); - switch (host.Window) - { - // Legacy osuTK DesktopGameWindow - case OsuTKDesktopWindow desktopGameWindow: - desktopGameWindow.CursorState |= CursorState.Hidden; - desktopGameWindow.SetIconFromStream(iconStream); - desktopGameWindow.Title = Name; - desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames); - break; + var desktopWindow = (SDL2DesktopWindow)host.Window; - // SDL2 DesktopWindow - case SDL2DesktopWindow desktopWindow: - desktopWindow.CursorState |= CursorState.Hidden; - desktopWindow.SetIconFromStream(iconStream); - desktopWindow.Title = Name; - desktopWindow.DragDrop += f => fileDrop(new[] { f }); - break; - } + desktopWindow.CursorState |= CursorState.Hidden; + desktopWindow.SetIconFromStream(iconStream); + desktopWindow.Title = Name; + desktopWindow.DragDrop += f => fileDrop(new[] { f }); } private void fileDrop(string[] filePaths) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 6ca7079654..d06c4b6746 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -22,9 +22,8 @@ namespace osu.Desktop { // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; - bool useOsuTK = args.Contains("--tk"); - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK)) + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) { host.ExceptionThrown += handleException; diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 71f9fafe57..47cd39dc5a 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -42,7 +42,7 @@ namespace osu.Desktop.Updater Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); } - protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync(); + protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { @@ -51,9 +51,9 @@ namespace osu.Desktop.Updater try { - updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true); + updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false); - var info = await updateManager.CheckForUpdate(!useDeltaPatching); + var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); if (info.ReleasesToApply.Count == 0) { @@ -79,12 +79,12 @@ namespace osu.Desktop.Updater try { - await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f); + await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.Progress = 0; notification.Text = @"Installing update..."; - await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f); + await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.State = ProgressNotificationState.Completed; updatePending = true; @@ -97,7 +97,7 @@ namespace osu.Desktop.Updater // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) // try again without deltas. - await checkForUpdateAsync(false, notification); + await checkForUpdateAsync(false, notification).ConfigureAwait(false); scheduleRecheck = false; } else @@ -116,7 +116,7 @@ namespace osu.Desktop.Updater if (scheduleRecheck) { // check again in 30 minutes. - Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30); + Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30); } } diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj index 88b420ffad..2e6c10a02e 100644 --- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj +++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj @@ -35,5 +35,10 @@ <Name>osu.Game</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <PackageReference Include="System.Formats.Asn1"> + <Version>5.0.0</Version> + </PackageReference> + </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> </Project> \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index f15da29993..1248409b2a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests [BackgroundDependencyLoader] private void load() { - LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false); + LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false); } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index e8bb57cdf3..48efd73222 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -202,7 +202,7 @@ namespace osu.Game.Rulesets.Catch.Tests public void TestHitLightingColour() { var fruitColour = SkinConfiguration.DefaultComboColours[1]; - AddStep("enable hit lighting", () => config.Set(OsuSetting.HitLighting, true)); + AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true)); AddStep("catch fruit", () => attemptCatch(new Fruit())); AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour); @@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestHitLightingDisabled() { - AddStep("disable hit lighting", () => config.Set(OsuSetting.HitLighting, false)); + AddStep("disable hit lighting", () => config.SetValue(OsuSetting.HitLighting, false)); AddStep("catch fruit", () => attemptCatch(new Fruit())); AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any()); } diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index bf3aba5859..728af5124e 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ <Import Project="..\osu.TestProject.props" /> <ItemGroup Label="Package References"> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 0a817eca0d..f4ddbd3021 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using System; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Catch.Skinning.Legacy; using osu.Game.Skinning; @@ -50,40 +51,40 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new CatchModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new CatchModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new CatchModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new CatchModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new CatchModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new CatchModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new CatchModEasy(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new CatchModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new CatchModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new CatchModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new CatchModHidden(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new CatchModNoFail(); - if (mods.HasFlag(LegacyMods.Relax)) + if (mods.HasFlagFast(LegacyMods.Relax)) yield return new CatchModRelax(); } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index a317ef252d..10aae70722 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) { halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new Skill[] { - new Movement(halfCatcherWidth), + new Movement(mods, halfCatcherWidth), }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index e679231638..9ad719be1a 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Difficulty.Skills { @@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private double lastStrainTime; - public Movement(float halfCatcherWidth) + public Movement(Mod[] mods, float halfCatcherWidth) + : base(mods) { HalfCatcherWidth = halfCatcherWidth; } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 41fd0fe776..1b48832ed6 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK.Graphics; -using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { @@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy /// <summary> /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. /// </summary> - private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score"); + private bool providesComboCounter => this.HasFont(LegacyFont.Combo); public CatchLegacySkinTransformer(ISkinSource source) : base(source) @@ -69,7 +68,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy this.GetAnimation("fruit-ryuuta", true, true, true); case CatchSkinComponents.CatchComboCounter: - if (providesComboCounter) return new LegacyCatchComboCounter(Source); diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index f797ae75c2..28ee7bd813 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { @@ -22,9 +21,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy public LegacyCatchComboCounter(ISkin skin) { - var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score"; - var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f; - AutoSizeAxes = Axes.Both; Alpha = 0f; @@ -34,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy InternalChildren = new Drawable[] { - explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) + explosion = new LegacyRollingCounter(skin, LegacyFont.Combo) { Alpha = 0.65f, Blending = BlendingParameters.Additive, @@ -42,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(1.5f), }, - counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + counter = new LegacyRollingCounter(skin, LegacyFont.Combo) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj index 0e557cb260..8c134c7114 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj +++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj @@ -35,5 +35,10 @@ <Name>osu.Game</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <PackageReference Include="System.Formats.Asn1"> + <Version>5.0.0</Version> + </PackageReference> + </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> </Project> \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs index a5248c7712..399a46aa77 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); + Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); } @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); + Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); @@ -148,9 +148,9 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time"); + Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time"); Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time"); - Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time"); + Assert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released"); @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | | | var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); - beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 596430f9e5..7ae69bf7d7 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -5,11 +5,13 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; @@ -345,6 +347,14 @@ namespace osu.Game.Rulesets.Mania.Tests AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + + AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head); + AddAssert("head is visible", + () => currentPlayer.ChildrenOfType<DrawableHoldNote>() + .Single(note => note.HitObject == beatmap.HitObjects[0]) + .Head + .Alpha == 1); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } @@ -352,6 +362,8 @@ namespace osu.Game.Rulesets.Mania.Tests { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 6b8f5d5d9d..706268e478 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Tests } private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) - => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor); + => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor); private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index fcc0cafefc..af16f39563 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ <Import Project="..\osu.TestProject.props" /> <ItemGroup Label="Package References"> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 30d33de06e..c81710ed18 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; @@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); @@ -149,7 +150,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0); @@ -157,13 +158,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2.5) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0); } - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0); @@ -221,7 +222,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); int lastColumn = nextColumn; @@ -373,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; - bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability); + bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); if (canGenerateTwoNotes) @@ -406,7 +407,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int endTime = startTime + SegmentDuration * SpanCount; int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); for (int i = 0; i < columnRepeat; i++) @@ -435,7 +436,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); // Create the hold note diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index bc4ab55767..54c37e9742 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osuTK; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy else convertType |= PatternType.LowProbability; - if (!convertType.HasFlag(PatternType.KeepSingle)) + if (!convertType.HasFlagFast(PatternType.KeepSingle)) { if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) convertType |= PatternType.Mirror; @@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; - if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by copying the last hit objects in reverse-column order for (int i = RandomStart; i < TotalColumns; i++) @@ -113,11 +114,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 - // If we convert to 7K + 1, let's not overload the special key - && (TotalColumns != 8 || lastColumn != 0) - // Make sure the last column was not the centre column - && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) + if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 + // If we convert to 7K + 1, let's not overload the special key + && (TotalColumns != 8 || lastColumn != 0) + // Make sure the last column was not the centre column + && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) { // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) int column = RandomStart + TotalColumns - lastColumn - 1; @@ -126,7 +127,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by placing on the already filled columns for (int i = RandomStart; i < TotalColumns; i++) @@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (PreviousPattern.HitObjects.Count() == 1) { - if (convertType.HasFlag(PatternType.Stair)) + if (convertType.HasFlagFast(PatternType.Stair)) { // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" int targetColumn = lastColumn + 1; @@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlag(PatternType.ReverseStair)) + if (convertType.HasFlagFast(PatternType.ReverseStair)) { // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" int targetColumn = lastColumn - 1; @@ -163,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } } - if (convertType.HasFlag(PatternType.KeepSingle)) + if (convertType.HasFlagFast(PatternType.KeepSingle)) return generateRandomNotes(1); - if (convertType.HasFlag(PatternType.Mirror)) + if (convertType.HasFlagFast(PatternType.Mirror)) { if (ConversionDifficulty > 6.5) return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); @@ -178,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(1, 0.62, 0, 0); @@ -186,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0); @@ -194,7 +195,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0); @@ -207,9 +208,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy foreach (var obj in p.HitObjects) { - if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1) + if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1) StairType = PatternType.ReverseStair; - if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart) + if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart) StairType = PatternType.Stair; } @@ -229,7 +230,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { var pattern = new Pattern(); - bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack); + bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack); if (!allowStacking) noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); @@ -249,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int getNextColumn(int last) { - if (convertType.HasFlag(PatternType.Gathered)) + if (convertType.HasFlagFast(PatternType.Gathered)) { last++; if (last == TotalColumns) @@ -296,7 +297,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { - if (convertType.HasFlag(PatternType.ForceNotStack)) + if (convertType.HasFlagFast(PatternType.ForceNotStack)) return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); var pattern = new Pattern(); diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 756f2b7b2f..39d0f4bae4 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Mania.Configuration { base.InitialiseDefaults(); - Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5); - Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); + SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5); + SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ade830764d..8c0b9ed8b7 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input; - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Strain(((ManiaBeatmap)beatmap).TotalColumns) + new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 7ebc1ff752..d6ea58ee78 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -6,6 +6,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Difficulty.Skills @@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills private double individualStrain; private double overallStrain; - public Strain(int totalColumns) + public Strain(Mod[] mods, int totalColumns) + : base(mods) { holdEndTimes = new double[totalColumns]; individualStrains = new double[totalColumns]; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 324670c4b2..d9570bf8be 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -119,5 +119,8 @@ namespace osu.Game.Rulesets.Mania.Edit beatSnapGrid.SelectionTimeRange = null; } } + + public override string ConvertSelectionToString() + => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 50629f41a9..2689ed4112 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit int minColumn = int.MaxValue; int maxColumn = int.MinValue; + // find min/max in an initial pass before actually performing the movement. foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>()) { if (obj.Column < minColumn) @@ -55,8 +56,11 @@ namespace osu.Game.Rulesets.Mania.Edit columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn); - foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>()) - obj.Column += columnDelta; + EditorBeatmap.PerformOnSelection(h => + { + if (h is ManiaHitObject maniaObj) + maniaObj.Column += columnDelta; + }); } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4c729fef83..d624e094ad 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -59,76 +60,76 @@ namespace osu.Game.Rulesets.Mania public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new ManiaModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new ManiaModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new ManiaModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new ManiaModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new ManiaModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new ManiaModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new ManiaModEasy(); - if (mods.HasFlag(LegacyMods.FadeIn)) + if (mods.HasFlagFast(LegacyMods.FadeIn)) yield return new ManiaModFadeIn(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new ManiaModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new ManiaModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new ManiaModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new ManiaModHidden(); - if (mods.HasFlag(LegacyMods.Key1)) + if (mods.HasFlagFast(LegacyMods.Key1)) yield return new ManiaModKey1(); - if (mods.HasFlag(LegacyMods.Key2)) + if (mods.HasFlagFast(LegacyMods.Key2)) yield return new ManiaModKey2(); - if (mods.HasFlag(LegacyMods.Key3)) + if (mods.HasFlagFast(LegacyMods.Key3)) yield return new ManiaModKey3(); - if (mods.HasFlag(LegacyMods.Key4)) + if (mods.HasFlagFast(LegacyMods.Key4)) yield return new ManiaModKey4(); - if (mods.HasFlag(LegacyMods.Key5)) + if (mods.HasFlagFast(LegacyMods.Key5)) yield return new ManiaModKey5(); - if (mods.HasFlag(LegacyMods.Key6)) + if (mods.HasFlagFast(LegacyMods.Key6)) yield return new ManiaModKey6(); - if (mods.HasFlag(LegacyMods.Key7)) + if (mods.HasFlagFast(LegacyMods.Key7)) yield return new ManiaModKey7(); - if (mods.HasFlag(LegacyMods.Key8)) + if (mods.HasFlagFast(LegacyMods.Key8)) yield return new ManiaModKey8(); - if (mods.HasFlag(LegacyMods.Key9)) + if (mods.HasFlagFast(LegacyMods.Key9)) yield return new ManiaModKey9(); - if (mods.HasFlag(LegacyMods.KeyCoop)) + if (mods.HasFlagFast(LegacyMods.KeyCoop)) yield return new ManiaModDualStages(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new ManiaModNoFail(); - if (mods.HasFlag(LegacyMods.Random)) + if (mods.HasFlagFast(LegacyMods.Random)) yield return new ManiaModRandom(); - if (mods.HasFlag(LegacyMods.Mirror)) + if (mods.HasFlagFast(LegacyMods.Mirror)) yield return new ManiaModMirror(); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index cbdcd49c5b..f80c9e1f7c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -1,18 +1,20 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; +using System; +using System.Linq; using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModFadeIn : ManiaModHidden + public class ManiaModFadeIn : ManiaModPlayfieldCover { public override string Name => "Fade In"; public override string Acronym => "FI"; - public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 4bdb15526f..e3ac624a6e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -3,43 +3,17 @@ using System; using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject> + public class ManiaModHidden : ManiaModPlayfieldCover { public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) }; - /// <summary> - /// The direction in which the cover should expand. - /// </summary> - protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); - public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset) - { - ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; - - foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) - { - HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; - Container hocParent = (Container)hoc.Parent; - - hocParent.Remove(hoc); - hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => - { - c.RelativeSizeAxes = Axes.Both; - c.Direction = ExpandDirection; - c.Coverage = 0.5f; - })); - } - } + protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs new file mode 100644 index 0000000000..87501d07a5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject> + { + public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) }; + + /// <summary> + /// The direction in which the cover should expand. + /// </summary> + protected abstract CoverExpandDirection ExpandDirection { get; } + + public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset) + { + ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; + + foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) + { + HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + Container hocParent = (Container)hoc.Parent; + + hocParent.Remove(hoc); + hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => + { + c.RelativeSizeAxes = Axes.Both; + c.Direction = ExpandDirection; + c.Coverage = 0.5f; + })); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index 75dcf0e55e..35ba2465fa 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects.Drawables; + namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// <summary> @@ -25,6 +27,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables LifetimeEnd = LifetimeStart + 30000; } + protected override void UpdateHitStateTransforms(ArmedState state) + { + // suppress the base call explicitly. + // the hold note head should never change its visual state on its own due to the "freezing" mechanic + // (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line). + // it will be hidden along with its parenting hold note when required. + } + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note public override void OnReleased(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 3ebbe5af8e..7c51d58b74 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; @@ -85,20 +86,28 @@ namespace osu.Game.Rulesets.Mania.Replays { var currentObject = Beatmap.HitObjects[i]; var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button - - double endTime = currentObject.GetEndTime(); - - bool canDelayKeyUp = nextObjectInColumn == null || - nextObjectInColumn.StartTime > endTime + RELEASE_DELAY; - - double calculatedDelay = canDelayKeyUp ? RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9; + var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn); yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column }; - yield return new ReleasePoint { Time = endTime + calculatedDelay, Column = currentObject.Column }; + yield return new ReleasePoint { Time = releaseTime, Column = currentObject.Column }; } } + private double calculateReleaseTime(HitObject currentObject, HitObject nextObject) + { + double endTime = currentObject.GetEndTime(); + + if (currentObject is HoldNote) + // hold note releases must be timed exactly. + return endTime; + + bool canDelayKeyUpFully = nextObject == null || + nextObject.StartTime > endTime + RELEASE_DELAY; + + return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9); + } + protected override HitObject GetNextObject(int currentIndex) { int desiredColumn = Beatmap.HitObjects[currentIndex].Column; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs index 73aece1ed4..e4d466dca5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion { + public const double FADE_IN_DURATION = 80; + private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private Drawable explosion; @@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy (explosion as IFramedAnimation)?.GotoFrame(0); - explosion?.FadeInFromZero(80) + explosion?.FadeInFromZero(FADE_IN_DURATION) .Then().FadeOut(120); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs index 78ccb83a8c..10319a7d4d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs @@ -101,8 +101,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { if (action == column.Action.Value) { - upSprite.FadeTo(1); - downSprite.FadeTo(0); + upSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(1); + downSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(0); } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index cbbbacfe19..24ccae895d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return animation == null ? null : new LegacyManiaJudgementPiece(result, animation); } - public override Sample GetSample(ISampleInfo sampleInfo) + public override ISample GetSample(ISampleInfo sampleInfo) { // layered hit sounds never play in mania if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered) diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj index dcf1573522..22fa605176 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj +++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj @@ -35,5 +35,10 @@ <Name>osu.Game</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <PackageReference Include="System.Formats.Asn1"> + <Version>5.0.0</Version> + </PackageReference> + </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> </Project> \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index a365ea10d4..c2119585ab 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.9311451172608853d, "diffcalc-test")] - [TestCase(1.0736587013228804d, "zero-length-sliders")] + [TestCase(6.9311451172574934d, "diffcalc-test")] + [TestCase(1.0736586907780401d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.6228371119393064d, "diffcalc-test")] - [TestCase(1.2864585434597433d, "zero-length-sliders")] + [TestCase(8.6228371119271454d, "diffcalc-test")] + [TestCase(1.2864585280364178d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png new file mode 100644 index 0000000000..8304617d8c Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png new file mode 100644 index 0000000000..c3b85eb873 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png new file mode 100644 index 0000000000..7f65eb7ca7 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png new file mode 100644 index 0000000000..82bec3babe Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png new file mode 100644 index 0000000000..5e38c75a9d Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png new file mode 100644 index 0000000000..a562d9f2ac Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png new file mode 100644 index 0000000000..b4cf81f26e Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png new file mode 100644 index 0000000000..a23f5379b2 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png new file mode 100644 index 0000000000..430b18509d Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png new file mode 100644 index 0000000000..add1202c31 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png new file mode 100644 index 0000000000..f68d32957f Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png new file mode 100644 index 0000000000..80c39b8745 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png new file mode 100644 index 0000000000..fc750abc7e Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png new file mode 100644 index 0000000000..779773f8bd Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini index 5369de24e9..89bcd68343 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini @@ -1,2 +1,6 @@ [General] -Version: 1.0 \ No newline at end of file +Version: 1.0 + +[Fonts] +HitCircleOverlap: 3 +ScoreOverlap: 3 \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index e2d9f144c0..8fd13c7417 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests return null; } - public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index e4158d8f07..4395ca6281 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestHitLightingDisabled() { - AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false)); + AddStep("hit lighting disabled", () => config.SetValue(OsuSetting.HitLighting, false)); showResult(HitResult.Great); @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestHitLightingEnabled() { - AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true)); + AddStep("hit lighting enabled", () => config.SetValue(OsuSetting.HitLighting, true)); showResult(HitResult.Great); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 461779b185..e3ccf83715 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddSliderStep("circle size", 0f, 10f, 0f, val => { - config.Set(OsuSetting.AutoCursorSize, true); + config.SetValue(OsuSetting.AutoCursorSize, true); gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; Scheduler.AddOnce(recreate); }); @@ -64,21 +64,21 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(10, 1.5f)] public void TestSizing(int circleSize, float userScale) { - AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale)); + AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); - AddStep("turn on autosizing", () => config.Set(OsuSetting.AutoCursorSize, true)); + AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); AddStep("load content", loadContent); AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale); - AddStep("set user scale to 1", () => config.Set(OsuSetting.GameplayCursorSize, 1f)); + AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f)); AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize)); - AddStep("turn off autosizing", () => config.Set(OsuSetting.AutoCursorSize, false)); + AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false)); AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1); - AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale)); + AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 8dbb48c048..6c6f05c5c5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable user provider", () => testUserSkin.Enabled = true); - AddStep("enable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, true)); + AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true)); checkNextHitObject("beatmap"); - AddStep("disable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, false)); + AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false)); checkNextHitObject("user"); AddStep("disable user provider", () => testUserSkin.Enabled = false); @@ -57,20 +57,20 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable user provider", () => testUserSkin.Enabled = true); - AddStep("enable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, true)); - AddStep("enable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, true)); + AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true)); + AddStep("enable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, true)); checkNextHitObject("beatmap"); - AddStep("enable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, true)); - AddStep("disable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, false)); + AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true)); + AddStep("disable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, false)); checkNextHitObject("beatmap"); - AddStep("disable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, false)); - AddStep("enable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, true)); + AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false)); + AddStep("enable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, true)); checkNextHitObject("user"); - AddStep("disable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, false)); - AddStep("disable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, false)); + AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false)); + AddStep("disable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, false)); checkNextHitObject("user"); AddStep("disable user provider", () => testUserSkin.Enabled = false); @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Tests public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; - public Sample GetSample(ISampleInfo sampleInfo) => null; + public ISample GetSample(ISampleInfo sampleInfo) => null; public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default; public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null; diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index b4c686ccea..3d2d1f3fec 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ <Import Project="..\osu.TestProject.props" /> <ItemGroup Label="Package References"> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index e8272057f3..9589fd576f 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Configuration protected override void InitialiseDefaults() { base.InitialiseDefaults(); - Set(OsuRulesetSetting.SnakingInSliders, true); - Set(OsuRulesetSetting.SnakingOutSliders, true); - Set(OsuRulesetSetting.ShowCursorTrail, true); - Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); + SetDefault(OsuRulesetSetting.SnakingInSliders, true); + SetDefault(OsuRulesetSetting.SnakingOutSliders, true); + SetDefault(OsuRulesetSetting.ShowCursorTrail, true); + SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 6a7d76151c..75d6786d95 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Aim(), - new Speed() + new Aim(mods), + new Speed(mods) }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index e74f4933b2..90cba13c7c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -17,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double angle_bonus_begin = Math.PI / 3; private const double timing_threshold = 107; + public Aim(Mod[] mods) + : base(mods) + { + } + protected override double SkillMultiplier => 26.25; protected override double StrainDecayBase => 0.15; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 01f2fb8dc8..200bc7997d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double max_speed_bonus = 45; // ~330BPM private const double speed_balancing_factor = 40; + public Speed(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index e9838de63d..1390675a1a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -7,11 +7,13 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osuTK; @@ -23,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// <summary> /// A visualisation of a single <see cref="PathControlPoint"/> in a <see cref="Slider"/>. /// </summary> - public class PathControlPointPiece : BlueprintPiece<Slider> + public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip { public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; @@ -195,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components markerRing.Alpha = IsSelected.Value ? 1 : 0; - Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow; + Color4 colour = getColourFromNodeType(); if (IsHovered || IsSelected.Value) colour = colour.Lighten(1); @@ -203,5 +205,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components marker.Colour = colour; marker.Scale = new Vector2(slider.Scale); } + + private Color4 getColourFromNodeType() + { + if (!(ControlPoint.Type.Value is PathType pathType)) + return colours.Yellow; + + switch (pathType) + { + case PathType.Catmull: + return colours.Seafoam; + + case PathType.Bezier: + return colours.Pink; + + case PathType.PerfectCurve: + return colours.PurpleDark; + + default: + return colours.Red; + } + } + + public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 3d3dff653a..ba9bb3c485 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected SliderBodyPiece BodyPiece { get; private set; } protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; } protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; } + + [CanBeNull] protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } private readonly DrawableSlider slider; @@ -114,6 +117,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // throw away frame buffers on deselection. ControlPointVisualiser?.Expire(); + ControlPointVisualiser = null; + BodyPiece.RecyclePath(); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0490e8b8ce..396fd41377 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Osu.Edit protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(this); + public override string ConvertSelectionToString() + => string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index e58aacd86e..9f77175398 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.Judgements /// </example> public float RateAdjustedRotation; + /// <summary> + /// Time instant at which the spin was started (the first user input which caused an increase in spin). + /// </summary> + public double? TimeStarted; + /// <summary> /// Time instant at which the spinner has been completed (the user has executed all required spins). /// Will be null if all required spins haven't been completed. diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 77de0cb45b..aac830801b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay) }; public bool PerformFail() => false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index df0a41455f..4b0939db16 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModTraceable : ModWithVisibilityAdjustment + public class OsuModTraceable : ModWithVisibilityAdjustment { public override string Name => "Traceable"; public override string Acronym => "TC"; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 77094f928b..189003875d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -164,28 +164,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApproachCircle.Expire(true); } + protected override void UpdateStartTimeStateTransforms() + { + base.UpdateStartTimeStateTransforms(); + + ApproachCircle.FadeOut(50); + } + protected override void UpdateHitStateTransforms(ArmedState state) { Debug.Assert(HitObject.HitWindows != null); + // todo: temporary / arbitrary, used for lifetime optimisation. + this.Delay(800).FadeOut(); + switch (state) { case ArmedState.Idle: - this.Delay(HitObject.TimePreempt).FadeOut(500); HitArea.HitAction = null; break; case ArmedState.Miss: - ApproachCircle.FadeOut(50); this.FadeOut(100); break; - - case ArmedState.Hit: - ApproachCircle.FadeOut(50); - - // todo: temporary / arbitrary - this.Delay(800).FadeOut(); - break; } Expire(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d02376b6c3..39e78a14aa 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -33,12 +33,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public SpinnerSpmCounter SpmCounter { get; private set; } private Container<DrawableSpinnerTick> ticks; - private SpinnerBonusDisplay bonusDisplay; private PausableSkinnableSound spinningSample; private Bindable<bool> isSpinning; private bool spinnerFrequencyModulate; + /// <summary> + /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. + /// </summary> + public IBindable<double> GainedBonus => gainedBonus; + + private readonly Bindable<double> gainedBonus = new Bindable<double>(); + + private const double fade_out_duration = 160; + public DrawableSpinner() : this(null) { @@ -65,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()), + new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()), RotationTracker = new SpinnerRotationTracker(this) } }, @@ -76,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Y = 120, Alpha = 0 }, - bonusDisplay = new SpinnerBonusDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = -120, - }, spinningSample = new PausableSkinnableSound { Volume = { Value = 0 }, @@ -131,12 +133,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (tracking.NewValue) { if (!spinningSample.IsPlaying) - spinningSample?.Play(); - spinningSample?.VolumeTo(1, 300); + spinningSample.Play(); + + spinningSample.VolumeTo(1, 300); } else { - spinningSample?.VolumeTo(0, 300).OnComplete(_ => spinningSample.Stop()); + spinningSample.VolumeTo(0, fade_out_duration); } } @@ -158,11 +161,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override void UpdateStartTimeStateTransforms() + { + base.UpdateStartTimeStateTransforms(); + + if (Result?.TimeStarted is double startTime) + { + using (BeginAbsoluteSequence(startTime)) + fadeInCounter(); + } + } + protected override void UpdateHitStateTransforms(ArmedState state) { base.UpdateHitStateTransforms(state); - this.FadeOut(160).Expire(); + this.FadeOut(fade_out_duration).OnComplete(_ => + { + // looping sample should be stopped here as it is safer than running in the OnComplete + // of the volume transition above. + spinningSample.Stop(); + }); + + Expire(); // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. isSpinning?.TriggerChange(); @@ -262,13 +283,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.UpdateAfterChildren(); if (!SpmCounter.IsPresent && RotationTracker.Tracking) - SpmCounter.FadeIn(HitObject.TimeFadeIn); + { + Result.TimeStarted ??= Time.Current; + fadeInCounter(); + } - SpmCounter.SetRotation(Result.RateAdjustedRotation); + // don't update after end time to avoid the rate display dropping during fade out. + // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. + if (Time.Current <= HitObject.EndTime) + SpmCounter.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); } + private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn); + + private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; + private int wholeSpins; private void updateBonusScore() @@ -293,8 +324,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (tick != null) { tick.TriggerResult(true); + if (tick is DrawableSpinnerBonusTick) - bonusDisplay.SetBonusCount(spins - HitObject.SpinsRequired); + gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired); } wholeSpins++; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 18324a18a8..838d707d64 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,7 @@ using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Statistics; @@ -58,52 +59,52 @@ namespace osu.Game.Rulesets.Osu public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new OsuModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new OsuModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new OsuModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new OsuModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Autopilot)) + if (mods.HasFlagFast(LegacyMods.Autopilot)) yield return new OsuModAutopilot(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new OsuModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new OsuModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new OsuModEasy(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new OsuModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new OsuModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new OsuModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new OsuModHidden(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new OsuModNoFail(); - if (mods.HasFlag(LegacyMods.Relax)) + if (mods.HasFlagFast(LegacyMods.Relax)) yield return new OsuModRelax(); - if (mods.HasFlag(LegacyMods.SpunOut)) + if (mods.HasFlagFast(LegacyMods.SpunOut)) yield return new OsuModSpunOut(); - if (mods.HasFlag(LegacyMods.Target)) + if (mods.HasFlagFast(LegacyMods.Target)) yield return new OsuModTarget(); - if (mods.HasFlag(LegacyMods.TouchDevice)) + if (mods.HasFlagFast(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 2883f0c187..fcb544fa5b 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Osu SliderFollowCircle, SliderBall, SliderBody, - SpinnerBody + SpinnerBody, } } diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json index 96e4bf1637..1a0bd66246 100644 --- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json @@ -1,5 +1,18 @@ { "Mappings": [{ + "StartTime": 114993, + "Objects": [{ + "StartTime": 114993, + "EndTime": 114993, + "X": 493, + "Y": 92 + }, { + "StartTime": 115290, + "EndTime": 115290, + "X": 451.659241, + "Y": 267.188 + }] + }, { "StartTime": 118858.0, "Objects": [{ "StartTime": 118858.0, diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu index 8c3edc9571..dd35098502 100644 --- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu @@ -9,7 +9,9 @@ SliderMultiplier:1.87 SliderTickRate:1 [TimingPoints] -49051,230.769230769231,4,2,1,15,1,0 +114000,346.820809248555,4,2,1,71,1,0 +118000,230.769230769231,4,2,1,15,1,0 [HitObjects] +493,92,114993,2,0,P|472:181|442:308,1,180,12|0,0:0|0:0,0:0:0:0: 219,215,118858,2,0,P|224:170|244:-10,1,187,8|2,0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs new file mode 100644 index 0000000000..891821fe2f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public class DefaultSpinner : CompositeDrawable + { + private DrawableSpinner drawableSpinner; + + private OsuSpriteText bonusCounter; + + public DefaultSpinner() + { + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject) + { + drawableSpinner = (DrawableSpinner)drawableHitObject; + + AddRangeInternal(new Drawable[] + { + new DefaultSpinnerDisc + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + bonusCounter = new OsuSpriteText + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: 24), + Y = -120, + } + }); + } + + private IBindable<double> gainedBonus; + + protected override void LoadComplete() + { + base.LoadComplete(); + + gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy(); + gainedBonus.BindValueChanged(bonus => + { + bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo); + bonusCounter.FadeOutFromOne(1500); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + }); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs index 667fee1495..542f3eff0d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs @@ -40,14 +40,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public DefaultSpinnerDisc() { - RelativeSizeAxes = Axes.Both; - // we are slightly bigger than our parent, to clip the top and bottom of the circle // this should probably be revisited when scaled spinners are a thing. Scale = new Vector2(initial_scale); - - Anchor = Anchor.Centre; - Origin = Anchor.Centre; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index fcbe4c1b28..46aeadc59b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -74,10 +74,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private void updateState(DrawableHitObject drawableObject, ArmedState state) { - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) - { + using (BeginAbsoluteSequence(drawableObject.StateUpdateTime)) glow.FadeOut(400); + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + { switch (state) { case ArmedState.Hit: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs index bea6186501..43d8d1e27f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public string Text { - get => number.Text; + get => number.Text.ToString(); set => number.Text = value; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs deleted file mode 100644 index c0db6228ef..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Osu.Objects; - -namespace osu.Game.Rulesets.Osu.Skinning.Default -{ - /// <summary> - /// Shows incremental bonus score achieved for a spinner. - /// </summary> - public class SpinnerBonusDisplay : CompositeDrawable - { - private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult; - - private readonly OsuSpriteText bonusCounter; - - public SpinnerBonusDisplay() - { - AutoSizeAxes = Axes.Both; - - InternalChild = bonusCounter = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Numeric.With(size: 24), - Alpha = 0, - }; - } - - private int displayedCount; - - public void SetBonusCount(int count) - { - if (displayedCount == count) - return; - - displayedCount = count; - bonusCounter.Text = $"{score_per_tick * count}"; - bonusCounter.FadeOutFromOne(1500); - bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index efeca53969..22fb3aab86 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy AddInternal(scaleContainer = new Container { Scale = new Vector2(SPRITE_SCALE), - Anchor = Anchor.Centre, + Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + Y = SPINNER_Y_CENTRE, Children = new Drawable[] { glow = new Sprite diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 4e07cb60b3..19cb55c16e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -33,47 +33,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; - AddInternal(new Container + AddRangeInternal(new Drawable[] { - // the old-style spinner relied heavily on absolute screen-space coordinate values. - // wrap everything in a container simulating absolute coords to preserve alignment - // as there are skins that depend on it. - Width = 640, - Height = 480, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + new Sprite { - new Sprite + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-background"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_Y_CENTRE, + }, + disc = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-circle"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_Y_CENTRE, + }, + metre = new Container + { + AutoSizeAxes = Axes.Both, + // this anchor makes no sense, but that's what stable uses. + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET }, + Masking = true, + Child = metreSprite = new Sprite { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-background"), - Scale = new Vector2(SPRITE_SCALE) - }, - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(SPRITE_SCALE) - }, - metre = new Container - { - AutoSizeAxes = Axes.Both, - // this anchor makes no sense, but that's what stable uses. + Texture = source.GetTexture("spinner-metre"), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - // adjustment for stable (metre has additional offset) - Margin = new MarginPadding { Top = 20 }, - Masking = true, - Child = metreSprite = new Sprite - { - Texture = source.GetTexture("spinner-metre"), - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Scale = new Vector2(SPRITE_SCALE) - } + Scale = new Vector2(SPRITE_SCALE) } } }); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index ec7ecb0d28..064b7a4680 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,6 +17,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public abstract class LegacySpinner : CompositeDrawable { + /// <remarks> + /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards. + /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space. + /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable) + /// </remarks> + protected const float SPINNER_TOP_OFFSET = 45f - 16f; + + protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; + protected const float SPRITE_SCALE = 0.625f; protected DrawableSpinner DrawableSpinner { get; private set; } @@ -23,43 +33,72 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Sprite spin; private Sprite clear; + private LegacySpriteText bonusCounter; + [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) { - RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + // osu!stable positions spinner components in window-space (as opposed to gamefield-space). This is a 640x480 area taking up the entire screen. + // In lazer, the gamefield-space positional transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to make this area take up the entire window space. + Size = new Vector2(640, 480); + Position = new Vector2(0, -8f); DrawableSpinner = (DrawableSpinner)drawableHitObject; - AddRangeInternal(new[] + AddInternal(new Container { - spin = new Sprite + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = float.MinValue, - Texture = source.GetTexture("spinner-spin"), - Scale = new Vector2(SPRITE_SCALE), - Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter - }, - clear = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = float.MinValue, - Alpha = 0, - Texture = source.GetTexture("spinner-clear"), - Scale = new Vector2(SPRITE_SCALE), - Y = -60 - }, + spin = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-spin"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 335, + }, + clear = new Sprite + { + Alpha = 0, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-clear"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 115, + }, + bonusCounter = new LegacySpriteText(source, LegacyFont.Score) + { + Alpha = 0f, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 299, + }.With(s => s.Font = s.Font.With(fixedWidth: false)), + } }); } + private IBindable<double> gainedBonus; + private readonly Bindable<bool> completed = new Bindable<bool>(); protected override void LoadComplete() { base.LoadComplete(); + gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy(); + gainedBonus.BindValueChanged(bonus => + { + bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo); + bonusCounter.FadeOutFromOne(800, Easing.Out); + bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out); + }); + completed.BindValueChanged(onCompletedChanged, true); DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index d74f885573..ffe238c507 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -97,17 +97,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; case OsuSkinComponents.HitCircleText: - var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; - var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2; + if (!this.HasFont(LegacyFont.HitCircle)) + return null; - return !this.HasFont(font) - ? null - : new LegacySpriteText(Source, font) - { - // stable applies a blanket 0.8x scale to hitcircle fonts - Scale = new Vector2(0.8f), - Spacing = new Vector2(-overlap, 0) - }; + return new LegacySpriteText(Source, LegacyFont.HitCircle) + { + // stable applies a blanket 0.8x scale to hitcircle fonts + Scale = new Vector2(0.8f), + }; case OsuSkinComponents.SpinnerBody: bool hasBackground = Source.GetTexture("spinner-background") != null; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 63c9b53278..75a62a6f8e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -5,8 +5,6 @@ namespace osu.Game.Rulesets.Osu.Skinning { public enum OsuSkinConfiguration { - HitCirclePrefix, - HitCircleOverlap, SliderBorderSize, SliderPathRadius, AllowSliderBallTint, diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj index 392442b713..a48110b354 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj +++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj @@ -35,5 +35,10 @@ <Name>osu.Game</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <PackageReference Include="System.Formats.Asn1"> + <Version>5.0.0</Version> + </PackageReference> + </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> </Project> \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index fecb5d4a74..61ea8b664d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -2,8 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; @@ -13,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneHitExplosion : TaikoSkinnableTestScene { + protected override double TimePerAction => 100; + [Test] public void TestNormalHit() { @@ -21,11 +27,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss)))); } - [Test] - public void TestStrongHit([Values(false, true)] bool hitBoth) + [TestCase(HitResult.Great)] + [TestCase(HitResult.Ok)] + public void TestStrongHit(HitResult type) { - AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth)))); - AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth)))); + AddStep("create hit", () => SetContents(() => getContentFor(createStrongHit(type)))); + AddStep("visualise second hit", + () => this.ChildrenOfType<HitExplosion>() + .ForEach(e => e.VisualiseSecondHit(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())))); } private Drawable getContentFor(DrawableTestHit hit) @@ -38,17 +47,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning // the hit needs to be added to hierarchy in order for nested objects to be created correctly. // setting zero alpha is supposed to prevent the test from looking broken. hit.With(h => h.Alpha = 0), - new HitExplosion(hit, hit.Type) + new HitExplosion(hit.Type) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - } + }.With(explosion => explosion.Apply(hit)) } }; } private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); - private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth); + private DrawableTestHit createStrongHit(HitResult type) => new DrawableTestStrongHit(Time.Current, type); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 7695ca067b..87c936d386 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -11,7 +11,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; @@ -108,12 +107,12 @@ namespace osu.Game.Rulesets.Taiko.Tests { HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; - Hit hit = new Hit(); + Hit hit = new Hit { StartTime = DrawableRuleset.Playfield.Time.Current }; var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult }); } private void addStrongHitJudgement(bool kiai) @@ -122,6 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit hit = new Hit { + StartTime = DrawableRuleset.Playfield.Time.Current, IsStrong = true, Samples = createSamples(strong: true) }; @@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Taiko.Tests DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); - ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(hit.NestedHitObjects.Single(), new TaikoStrongJudgement()) { Type = HitResult.Great }); } private void addMissJudgement() diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 2b084f3bee..fa00922706 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ <Import Project="..\osu.TestProject.props" /> <ItemGroup Label="Package References"> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 32421ee00a..cc0738e252 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// </summary> private int currentMonoLength; + public Colour(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { // changing from/to a drum roll or a swell does not constitute a colour change. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 5569b27ad5..f2b8309ac5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// </summary> private int notesSinceRhythmChange; + public Rhythm(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { // drum rolls and swells are exempt. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 0b61eb9930..c34cce0cd6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// <summary> /// Creates a <see cref="Stamina"/> skill. /// </summary> + /// <param name="mods">Mods for use in skill calculations.</param> /// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param> - public Stamina(bool rightHand) + public Stamina(Mod[] mods, bool rightHand) + : base(mods) { hand = rightHand ? 1 : 0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e5485db4df..fc198d2493 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Colour(), - new Rhythm(), - new Stamina(true), - new Stamina(false), + new Colour(mods), + new Rhythm(mods), + new Stamina(mods, true), + new Stamina(mods, false), }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 3fbcee44af..ac2dd4bdb6 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -52,32 +52,24 @@ namespace osu.Game.Rulesets.Taiko.Edit public void SetStrongState(bool state) { - var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>(); - - EditorBeatmap.BeginChange(); - - foreach (var h in hits) + EditorBeatmap.PerformOnSelection(h => { - if (h.IsStrong != state) - { - h.IsStrong = state; - EditorBeatmap.Update(h); - } - } + if (!(h is Hit taikoHit)) return; - EditorBeatmap.EndChange(); + if (taikoHit.IsStrong != state) + { + taikoHit.IsStrong = state; + EditorBeatmap.Update(taikoHit); + } + }); } public void SetRimState(bool state) { - var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>(); - - EditorBeatmap.BeginChange(); - - foreach (var h in hits) - h.Type = state ? HitType.Rim : HitType.Centre; - - EditorBeatmap.EndChange(); + EditorBeatmap.PerformOnSelection(h => + { + if (h is Hit taikoHit) taikoHit.Type = state ? HitType.Rim : HitType.Centre; + }); } protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs index 651cdd6438..21bd35ad22 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs @@ -1,22 +1,22 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { - public class LegacyHitExplosion : CompositeDrawable + public class LegacyHitExplosion : CompositeDrawable, IAnimatableHitExplosion { private readonly Drawable sprite; - private readonly Drawable strongSprite; - private DrawableStrongNestedHit nestedStrongHit; - private bool switchedToStrongSprite; + [CanBeNull] + private readonly Drawable strongSprite; /// <summary> /// Creates a new legacy hit explosion. @@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy /// </remarks> /// <param name="sprite">The normal legacy explosion sprite.</param> /// <param name="strongSprite">The strong legacy explosion sprite.</param> - public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null) + public LegacyHitExplosion(Drawable sprite, [CanBeNull] Drawable strongSprite = null) { this.sprite = sprite; this.strongSprite = strongSprite; } [BackgroundDependencyLoader] - private void load(DrawableHitObject judgedObject) + private void load() { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -56,45 +56,30 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy s.Origin = Anchor.Centre; })); } - - if (judgedObject is DrawableHit hit) - nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit; } - protected override void LoadComplete() + public void Animate(DrawableHitObject drawableHitObject) { - base.LoadComplete(); - const double animation_time = 120; + (sprite as IFramedAnimation)?.GotoFrame(0); + (strongSprite as IFramedAnimation)?.GotoFrame(0); + this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5); this.ScaleTo(0.6f) .Then().ScaleTo(1.1f, animation_time * 0.8) .Then().ScaleTo(0.9f, animation_time * 0.4) .Then().ScaleTo(1f, animation_time * 0.2); - - Expire(true); } - protected override void Update() + public void AnimateSecondHit() { - base.Update(); + if (strongSprite == null) + return; - if (shouldSwitchToStrongSprite() && !switchedToStrongSprite) - { - sprite.FadeOut(50, Easing.OutQuint); - strongSprite.FadeIn(50, Easing.OutQuint); - switchedToStrongSprite = true; - } - } - - private bool shouldSwitchToStrongSprite() - { - if (nestedStrongHit == null || strongSprite == null) - return false; - - return nestedStrongHit.IsHit; + sprite.FadeOut(50, Easing.OutQuint); + strongSprite.FadeIn(50, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 9f29675230..3e506f69ce 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { // if a taiko skin is providing explosion sprites, hide the judgements completely if (hasExplosion.Value) - return Drawable.Empty(); + return Drawable.Empty().With(d => d.Expire()); } if (!(component is TaikoSkinComponent taikoComponent)) @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy // suppress the default kiai explosion if the skin brings its own sprites. // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); + return Drawable.Empty().With(d => d.Expire()); return null; @@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}"); } - public override Sample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); + public override ISample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index f2b5d195b4..56f58f404b 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -57,43 +58,43 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new TaikoModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new TaikoModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new TaikoModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new TaikoModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new TaikoModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new TaikoModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new TaikoModEasy(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new TaikoModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new TaikoModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new TaikoModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new TaikoModHidden(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new TaikoModNoFail(); - if (mods.HasFlag(LegacyMods.Relax)) + if (mods.HasFlagFast(LegacyMods.Relax)) yield return new TaikoModRelax(); - if (mods.HasFlag(LegacyMods.Random)) + if (mods.HasFlagFast(LegacyMods.Random)) yield return new TaikoModRandom(); } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 3bd20e4bb4..91e844187a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,19 +14,23 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.UI { - internal class DefaultHitExplosion : CircularContainer + internal class DefaultHitExplosion : CircularContainer, IAnimatableHitExplosion { - private readonly DrawableHitObject judgedObject; private readonly HitResult result; - public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result) + [CanBeNull] + private Box body; + + [Resolved] + private OsuColour colours { get; set; } + + public DefaultHitExplosion(HitResult result) { - this.judgedObject = judgedObject; this.result = result; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { RelativeSizeAxes = Axes.Both; @@ -40,26 +45,36 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.IsHit()) return; - bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; - InternalChildren = new[] { - new Box + body = new Box { RelativeSizeAxes = Axes.Both, - Colour = isRim ? colours.BlueDarker : colours.PinkDarker, } }; + + updateColour(); } - protected override void LoadComplete() + private void updateColour([CanBeNull] DrawableHitObject judgedObject = null) { - base.LoadComplete(); + if (body == null) + return; + + bool isRim = (judgedObject?.HitObject as Hit)?.Type == HitType.Rim; + body.Colour = isRim ? colours.BlueDarker : colours.PinkDarker; + } + + public void Animate(DrawableHitObject drawableHitObject) + { + updateColour(drawableHitObject); this.ScaleTo(3f, 1000, Easing.OutQuint); this.FadeOut(500); + } - Expire(true); + public void AnimateSecondHit() + { } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index b5e35f88b5..1ad1e4495c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.UI { @@ -12,16 +11,6 @@ namespace osu.Game.Rulesets.Taiko.UI /// </summary> public class DrawableTaikoJudgement : DrawableJudgement { - /// <summary> - /// Creates a new judgement text. - /// </summary> - /// <param name="judgedObject">The object which is being judged.</param> - /// <param name="result">The judgement to visualise.</param> - public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject) - : base(result, judgedObject) - { - } - protected override void ApplyHitAnimations() { this.MoveToY(-100, 500); diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index d1fb3348b9..8f5e9e54ab 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; @@ -16,31 +18,37 @@ namespace osu.Game.Rulesets.Taiko.UI /// <summary> /// A circle explodes from the hit target to indicate a hitobject has been hit. /// </summary> - internal class HitExplosion : CircularContainer + internal class HitExplosion : PoolableDrawable { public override bool RemoveWhenNotAlive => true; - - [Cached(typeof(DrawableHitObject))] - public readonly DrawableHitObject JudgedObject; + public override bool RemoveCompletedTransforms => false; private readonly HitResult result; + private double? secondHitTime; + + [CanBeNull] + public DrawableHitObject JudgedObject; + private SkinnableDrawable skinnable; - public override double LifetimeStart => skinnable.Drawable.LifetimeStart; - - public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; - - public HitExplosion(DrawableHitObject judgedObject, HitResult result) + /// <summary> + /// This constructor only exists to meet the <c>new()</c> type constraint of <see cref="DrawablePool{T}"/>. + /// </summary> + public HitExplosion() + : this(HitResult.Great) + { + } + + public HitExplosion(HitResult result) { - JudgedObject = judgedObject; this.result = result; Anchor = Anchor.Centre; Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; Size = new Vector2(TaikoHitObject.DEFAULT_SIZE); + RelativeSizeAxes = Axes.Both; RelativePositionAxes = Axes.Both; } @@ -48,7 +56,47 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(JudgedObject, result)); + InternalChild = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(result)); + skinnable.OnSkinChanged += runAnimation; + } + + public void Apply([CanBeNull] DrawableHitObject drawableHitObject) + { + JudgedObject = drawableHitObject; + secondHitTime = null; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + runAnimation(); + } + + private void runAnimation() + { + if (JudgedObject?.Result == null) + return; + + double resultTime = JudgedObject.Result.TimeAbsolute; + + LifetimeStart = resultTime; + + ApplyTransformsAt(double.MinValue, true); + ClearTransforms(true); + + using (BeginAbsoluteSequence(resultTime)) + (skinnable.Drawable as IAnimatableHitExplosion)?.Animate(JudgedObject); + + if (secondHitTime != null) + { + using (BeginAbsoluteSequence(secondHitTime.Value)) + { + this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50); + (skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit(); + } + } + + LifetimeEnd = skinnable.Drawable.LatestTransformEndTime; } private static TaikoSkinComponents getComponentName(HitResult result) @@ -68,12 +116,10 @@ namespace osu.Game.Rulesets.Taiko.UI throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}"); } - /// <summary> - /// Transforms this hit explosion to visualise a secondary hit. - /// </summary> - public void VisualiseSecondHit() + public void VisualiseSecondHit(JudgementResult judgementResult) { - this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50); + secondHitTime = judgementResult.TimeAbsolute; + runAnimation(); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs new file mode 100644 index 0000000000..badf34554c --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.UI +{ + /// <summary> + /// Pool for hit explosions of a specific type. + /// </summary> + internal class HitExplosionPool : DrawablePool<HitExplosion> + { + private readonly HitResult hitResult; + + public HitExplosionPool(HitResult hitResult) + : base(15) + { + this.hitResult = hitResult; + } + + protected override HitExplosion CreateNewDrawable() => new HitExplosion(hitResult); + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs new file mode 100644 index 0000000000..cf0f5f9fb6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.UI +{ + /// <summary> + /// A skinnable element of a hit explosion that supports playing an animation from the current point in time. + /// </summary> + public interface IAnimatableHitExplosion + { + /// <summary> + /// Shows the hit explosion for the supplied <paramref name="drawableHitObject"/>. + /// </summary> + void Animate(DrawableHitObject drawableHitObject); + + /// <summary> + /// Transforms the hit explosion to visualise a secondary hit. + /// </summary> + void AnimateSecondHit(); + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 148ec7755e..46dafc3a30 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -17,6 +19,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Skinning; using osuTK; @@ -38,6 +41,9 @@ namespace osu.Game.Rulesets.Taiko.UI internal Drawable HitTarget; private SkinnableDrawable mascot; + private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>(); + private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>(); + private ProxyContainer topLevelHitContainer; private Container rightArea; private Container leftArea; @@ -159,6 +165,17 @@ namespace osu.Game.Rulesets.Taiko.UI RegisterPool<Swell, DrawableSwell>(5); RegisterPool<SwellTick, DrawableSwellTick>(100); + + var hitWindows = new TaikoHitWindows(); + + foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))) + { + judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15)); + explosionPools.Add(result, new HitExplosionPool(result)); + } + + AddRangeInternal(judgementPools.Values); + AddRangeInternal(explosionPools.Values); } protected override void LoadComplete() @@ -270,7 +287,7 @@ namespace osu.Game.Rulesets.Taiko.UI { case TaikoStrongJudgement _: if (result.IsHit) - hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(); + hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result); break; case TaikoDrumRollTickJudgement _: @@ -283,13 +300,15 @@ namespace osu.Game.Rulesets.Taiko.UI break; default: - judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) + judgementContainer.Add(judgementPools[result.Type].Get(j => { - Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, - Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre, - RelativePositionAxes = Axes.X, - X = result.IsHit ? judgedObject.Position.X : 0, - }); + j.Apply(result, judgedObject); + + j.Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft; + j.Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre; + j.RelativePositionAxes = Axes.X; + j.X = result.IsHit ? judgedObject.Position.X : 0; + })); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; addExplosion(judgedObject, result.Type, type); @@ -302,7 +321,8 @@ namespace osu.Game.Rulesets.Taiko.UI private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) { - hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); + hitExplosionContainer.Add(explosionPools[result] + .Get(explosion => explosion.Apply(drawableObject))); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index 19e36a63f1..bf256f486c 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -20,6 +20,9 @@ <ItemGroup> <None Include="Properties\AndroidManifest.xml" /> </ItemGroup> + <PropertyGroup> + <NoWarn>$(NoWarn);CA2007</NoWarn> + </PropertyGroup> <ItemGroup> <Compile Include="..\osu.Game.Tests\**\Beatmaps\**\*.cs"> <Link>%(RecursiveDir)%(Filename)%(Extension)</Link> @@ -71,7 +74,10 @@ </ItemGroup> <ItemGroup Label="Package References"> <PackageReference Include="DeepEqual" Version="2.0.0" /> - <PackageReference Include="Moq" Version="4.16.0" /> + <PackageReference Include="Moq" Version="4.16.1" /> + <PackageReference Include="System.Formats.Asn1"> + <Version>5.0.0</Version> + </PackageReference> </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> -</Project> \ No newline at end of file +</Project> diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index 67b2298f4c..97df9b2cd5 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -21,6 +21,9 @@ <Link>%(RecursiveDir)%(Filename)%(Extension)</Link> </Compile> </ItemGroup> + <PropertyGroup> + <NoWarn>$(NoWarn);CA2007</NoWarn> + </PropertyGroup> <ItemGroup Label="Project References"> <ProjectReference Include="..\osu.Game\osu.Game.csproj"> <Project>{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}</Project> @@ -45,7 +48,7 @@ </ItemGroup> <ItemGroup Label="Package References"> <PackageReference Include="DeepEqual" Version="2.0.0" /> - <PackageReference Include="Moq" Version="4.16.0" /> + <PackageReference Include="Moq" Version="4.16.1" /> </ItemGroup> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> -</Project> \ No newline at end of file +</Project> diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c32e359de6..0c35e9471d 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -852,6 +852,21 @@ namespace osu.Game.Tests.Beatmaps.IO } } + public static async Task<BeatmapSetInfo> LoadQuickOszIntoOsu(OsuGameBase osu) + { + var temp = TestResources.GetQuickTestBeatmapForImport(); + + var manager = osu.Dependencies.Get<BeatmapManager>(); + + var importedSet = await manager.Import(new ImportTask(temp)); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + } + public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) { var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 3ded3009bd..883791c35c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Gameplay public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); - public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) { diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 7a0dd5b719..cae5f20332 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.IO; using osu.Game.Rulesets; @@ -35,7 +36,7 @@ namespace osu.Game.Tests.Gameplay public void TestRetrieveTopLevelSample() { ISkin skin = null; - Sample channel = null; + ISample channel = null; AddStep("create skin", () => skin = new TestSkin("test-sample", this)); AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample"))); @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Gameplay public void TestRetrieveSampleInSubFolder() { ISkin skin = null; - Sample channel = null; + ISample channel = null; AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this)); AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample"))); @@ -90,6 +91,7 @@ namespace osu.Game.Tests.Gameplay public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate) { GameplayClockContainer gameplayContainer = null; + StoryboardSampleInfo sampleInfo = null; TestDrawableStoryboardSample sample = null; Mod testedMod = Activator.CreateInstance(expectedMod) as Mod; @@ -101,7 +103,7 @@ namespace osu.Game.Tests.Gameplay break; case ModTimeRamp m: - m.InitialRate.Value = m.FinalRate.Value = expectedRate; + m.FinalRate.Value = m.InitialRate.Value = expectedRate; break; } @@ -117,7 +119,7 @@ namespace osu.Game.Tests.Gameplay Child = beatmapSkinSourceContainer }); - beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) + beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1)) { Clock = gameplayContainer.GameplayClock }); @@ -125,7 +127,10 @@ namespace osu.Game.Tests.Gameplay AddStep("start", () => gameplayContainer.Start()); - AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == expectedRate); + AddAssert("sample playback rate matches mod rates", () => + testedMod != null && Precision.AlmostEquals( + sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value, + ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime))); } private class TestSkin : LegacySkin diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index b90382488f..27cece42e8 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Input => AddStep($"make window {mode}", () => frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = mode); private void setGameSideModeTo(OsuConfineMouseMode mode) - => AddStep($"set {mode} game-side", () => Game.LocalConfig.Set(OsuSetting.ConfineMouseMode, mode)); + => AddStep($"set {mode} game-side", () => Game.LocalConfig.SetValue(OsuSetting.ConfineMouseMode, mode)); private void setLocalUserPlayingTo(bool playing) => AddStep($"local user {(playing ? "playing" : "not playing")}", () => Game.LocalUserPlaying.Value = playing); diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs new file mode 100644 index 0000000000..883c9d1ac2 --- /dev/null +++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Bindables; +using osu.Game.Configuration; + +namespace osu.Game.Tests.Mods +{ + [TestFixture] + public class SettingsSourceAttributeTest + { + [Test] + public void TestOrdering() + { + var objectWithSettings = new ClassWithSettings(); + + var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray(); + + Assert.That(orderedSettings, Has.Length.EqualTo(4)); + + Assert.That(orderedSettings[0].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.FirstSetting))); + Assert.That(orderedSettings[1].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.SecondSetting))); + Assert.That(orderedSettings[2].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.ThirdSetting))); + Assert.That(orderedSettings[3].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.UnorderedSetting))); + } + + private class ClassWithSettings + { + [SettingSource("Unordered setting", "Should be last")] + public BindableFloat UnorderedSetting { get; set; } = new BindableFloat(); + + [SettingSource("Second setting", "Another description", 2)] + public BindableBool SecondSetting { get; set; } = new BindableBool(); + + [SettingSource("First setting", "A description", 1)] + public BindableDouble FirstSetting { get; set; } = new BindableDouble(); + + [SettingSource("Third setting", "Yet another description", 3)] + public BindableInt ThirdSetting { get; set; } = new BindableInt(); + } + } +} diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 045246e5ed..a763544c37 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.NonVisual using (var host = new CustomTestHeadlessGameHost()) { using (var storageConfig = new StorageConfigManager(host.InitialStorage)) - storageConfig.Set(StorageConfig.FullPath, customPath); + storageConfig.SetValue(StorageConfig.FullPath, customPath); try { @@ -73,7 +73,7 @@ namespace osu.Game.Tests.NonVisual using (var host = new CustomTestHeadlessGameHost()) { using (var storageConfig = new StorageConfigManager(host.InitialStorage)) - storageConfig.Set(StorageConfig.FullPath, customPath); + storageConfig.SetValue(StorageConfig.FullPath, customPath); try { diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 5c7adb3f49..1c0bfd56dd 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -212,7 +212,7 @@ namespace osu.Game.Tests.NonVisual throw new NotImplementedException(); } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) { throw new NotImplementedException(); } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 24a0a662ba..8ff2743b6a 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -4,8 +4,10 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.NonVisual.Filtering { @@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + + [Test] + public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria) + { + var beatmap = getExampleBeatmap(); + + var customCriteria = matchCustomCriteria is bool match ? new CustomCriteria(match) : null; + var criteria = new FilterCriteria { RulesetCriteria = customCriteria }; + var carouselItem = new CarouselBeatmap(beatmap); + carouselItem.Filter(criteria); + + Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value); + } + + private class CustomCriteria : IRulesetFilterCriteria + { + private readonly bool match; + + public CustomCriteria(bool shouldMatch) + { + match = shouldMatch; + } + + public bool Matches(BeatmapInfo beatmap) => match; + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; + } } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index d15682b1eb..49389e67aa 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -4,7 +4,9 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.NonVisual.Filtering { @@ -194,5 +196,63 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm); } + + [Test] + public void TestOperatorParsing() + { + const string query = "artist=><something"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("><something", filterCriteria.Artist.SearchTerm); + } + + [Test] + public void TestUnrecognisedKeywordIsIgnored() + { + const string query = "unrecognised=keyword"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText); + } + + [TestCase("cs=nope")] + [TestCase("bpm>=bad")] + [TestCase("divisor<nah")] + [TestCase("status=noidea")] + public void TestInvalidKeywordValueIsIgnored(string query) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(query, filterCriteria.SearchText); + } + + [Test] + public void TestCustomKeywordIsParsed() + { + var customCriteria = new CustomFilterCriteria(); + const string query = "custom=readme unrecognised=keyword"; + var filterCriteria = new FilterCriteria { RulesetCriteria = customCriteria }; + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("readme", customCriteria.CustomValue); + Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim()); + } + + private class CustomFilterCriteria : IRulesetFilterCriteria + { + public string CustomValue { get; set; } + + public bool Matches(BeatmapInfo beatmap) => true; + + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + { + if (key == "custom" && op == Operator.Equal) + { + CustomValue = value; + return true; + } + + return false; + } + } } } diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs new file mode 100644 index 0000000000..df095ddee3 --- /dev/null +++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using NUnit.Framework; +using osu.Game.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class FormatUtilsTest + { + [TestCase(0, "0.00%")] + [TestCase(0.01, "1.00%")] + [TestCase(0.9899, "98.99%")] + [TestCase(0.989999, "98.99%")] + [TestCase(0.99, "99.00%")] + [TestCase(0.9999, "99.99%")] + [TestCase(0.999999, "99.99%")] + [TestCase(1, "100.00%")] + public void TestAccuracyFormatting(double input, string expectedOutput) + { + Assert.AreEqual(expectedOutput, input.FormatAccuracy(CultureInfo.InvariantCulture)); + } + } +} diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs index da004b9088..b08a228de3 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.NonVisual.Skinning } public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); - public Sample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException(); } diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index ab24a72a12..77f910c144 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Online var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod)); - Assert.That(deserialized.Acronym, Is.EqualTo(apiMod.Acronym)); + Assert.That(deserialized?.Acronym, Is.EqualTo(apiMod.Acronym)); } [Test] @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Online var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod)); - Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(2.0)); + Assert.That(deserialized?.Settings, Contains.Key("test_setting").With.ContainValue(2.0)); } [Test] @@ -44,9 +44,9 @@ namespace osu.Game.Tests.Online var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } }); var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod)); - var converted = (TestMod)deserialized.ToMod(new TestRuleset()); + var converted = (TestMod)deserialized?.ToMod(new TestRuleset()); - Assert.That(converted.TestSetting.Value, Is.EqualTo(2)); + Assert.That(converted?.TestSetting.Value, Is.EqualTo(2)); } [Test] @@ -61,11 +61,11 @@ namespace osu.Game.Tests.Online }); var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod)); - var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset()); + var converted = (TestModTimeRamp)deserialised?.ToMod(new TestRuleset()); - Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false)); - Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25)); - Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); + Assert.That(converted?.AdjustPitch.Value, Is.EqualTo(false)); + Assert.That(converted?.InitialRate.Value, Is.EqualTo(1.25)); + Assert.That(converted?.FinalRate.Value, Is.EqualTo(0.25)); } [Test] @@ -78,10 +78,10 @@ namespace osu.Game.Tests.Online }); var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod)); - var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset()); + var converted = (TestModDifficultyAdjust)deserialised?.ToMod(new TestRuleset()); - Assert.That(converted.ExtendedLimits.Value, Is.True); - Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11)); + Assert.That(converted?.ExtendedLimits.Value, Is.True); + Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11)); } private class TestRuleset : Ruleset diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index 42948c3731..aa29d76843 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -23,8 +23,10 @@ namespace osu.Game.Tests.Online { case CommentVoteRequest cRequest: cRequest.TriggerSuccess(new CommentBundle()); - break; + return true; } + + return false; }); CommentVoteRequest request = null; @@ -108,8 +110,10 @@ namespace osu.Game.Tests.Online { case LeaveChannelRequest cRequest: cRequest.TriggerSuccess(); - break; + return true; } + + return false; }); } } diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 3ffb512b7f..8c30802ce3 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Online { beatmaps.AllowImport = new TaskCompletionSource<bool>(); - testBeatmapFile = TestResources.GetTestBeatmapForImport(); + testBeatmapFile = TestResources.GetQuickTestBeatmapForImport(); testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs new file mode 100644 index 0000000000..82ce588c6f --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.OnlinePlay +{ + [HeadlessTest] + public class StatefulMultiplayerClientTest : MultiplayerTestScene + { + [Test] + public void TestUserAddedOnJoin() + { + var user = new User { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestUserRemovedOnLeave() + { + var user = new User { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + } +} diff --git a/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz new file mode 100644 index 0000000000..e9f5fb0328 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz differ diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index e882229570..cef0532f9d 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -15,6 +15,28 @@ namespace osu.Game.Tests.Resources public static Stream GetTestBeatmapStream(bool virtualTrack = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); + /// <summary> + /// Retrieve a path to a copy of a shortened (~10 second) beatmap archive with a virtual track. + /// </summary> + /// <remarks> + /// This is intended for use in tests which need to run to completion as soon as possible and don't need to test a full length beatmap.</remarks> + /// <returns>A path to a copy of a beatmap archive (osz). Should be deleted after use.</returns> + public static string GetQuickTestBeatmapForImport() + { + var tempPath = Path.GetTempFileName() + ".osz"; + using (var stream = OpenResource("Archives/241526 Soleily - Renatus_virtual_quick.osz")) + using (var newFile = File.Create(tempPath)) + stream.CopyTo(newFile); + + Assert.IsTrue(File.Exists(tempPath)); + return tempPath; + } + + /// <summary> + /// Retrieve a path to a copy of a full-fledged beatmap archive. + /// </summary> + /// <param name="virtualTrack">Whether the audio track should be virtual.</param> + /// <returns>A path to a copy of a beatmap archive (osz). Should be deleted after use.</returns> public static string GetTestBeatmapForImport(bool virtualTrack = false) { var tempPath = Path.GetTempFileName() + ".osz"; diff --git a/osu.Game.Tests/Resources/skin-with-space.ini b/osu.Game.Tests/Resources/skin-with-space.ini new file mode 100644 index 0000000000..3e64257a3e --- /dev/null +++ b/osu.Game.Tests/Resources/skin-with-space.ini @@ -0,0 +1,2 @@ +[General] +Version: 2 \ No newline at end of file diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 4aebed0d31..f421a30283 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -118,6 +118,10 @@ namespace osu.Game.Tests.Rulesets public BindableNumber<double> Frequency => throw new NotImplementedException(); public BindableNumber<double> Tempo => throw new NotImplementedException(); + public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + + public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException(); public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index a5b4b04ef5..8124bd4199 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -113,6 +113,31 @@ namespace osu.Game.Tests.Skins.IO } } + [Test] + public async Task TestImportUpperCasedOskArchive() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) + { + try + { + var osu = LoadOsuIntoHost(host); + + var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.OsK")); + + Assert.That(imported.Name, Is.EqualTo("name 1")); + Assert.That(imported.Creator, Is.EqualTo("author 1")); + + var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.oSK")); + + Assert.That(imported2.Hash, Is.EqualTo(imported.Hash)); + } + finally + { + host.Exit(); + } + } + } + private MemoryStream createOsk(string name, string author) { var zipStream = new MemoryStream(); diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index aedf26ee75..dcb866c99f 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -91,6 +91,15 @@ namespace osu.Game.Tests.Skins Assert.AreEqual(2.0m, decoder.Decode(stream).LegacyVersion); } + [Test] + public void TestStripWhitespace() + { + var decoder = new LegacySkinDecoder(); + using (var resStream = TestResources.OpenResource("skin-with-space.ini")) + using (var stream = new LineBufferedReader(resStream)) + Assert.AreEqual(2.0m, decoder.Decode(stream).LegacyVersion); + } + [Test] public void TestDecodeLatestVersion() { diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 414f7d3f88..732a3f3f42 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -219,7 +219,7 @@ namespace osu.Game.Tests.Skins public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT); - public Sample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); + public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup); } diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index fba0d92d4b..dc5a4f4a3e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Background public void SetUp() => Schedule(() => { // reset API response in statics to avoid test crosstalk. - statics.Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); + statics.SetValue<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); textureStore.PerformedLookups.Clear(); dummyAPI.SetState(APIState.Online); @@ -135,18 +135,20 @@ namespace osu.Game.Tests.Visual.Background dummyAPI.HandleRequest = request => { if (dummyAPI.State.Value != APIState.Online || !(request is GetSeasonalBackgroundsRequest backgroundsRequest)) - return; + return false; backgroundsRequest.TriggerSuccess(new APISeasonalBackgrounds { Backgrounds = seasonal_background_urls.Select(url => new APISeasonalBackground { Url = url }).ToList(), EndDate = endDate }); + + return true; }; }); private void setSeasonalBackgroundMode(SeasonalBackgroundMode mode) - => AddStep($"set seasonal mode to {mode}", () => config.Set(OsuSetting.SeasonalBackgroundMode, mode)); + => AddStep($"set seasonal mode to {mode}", () => config.SetValue(OsuSetting.SeasonalBackgroundMode, mode)); private void createLoader() => AddStep("create loader", () => diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 7ade7725d9..ba4d12b19f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Background Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - manager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + manager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index fef1605f0c..eca857f9e5 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -38,13 +38,13 @@ namespace osu.Game.Tests.Visual.Collections Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); - beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); base.Content.AddRange(new Drawable[] { manager = new CollectionManager(LocalStorage), Content, - dialogOverlay = new DialogOverlay() + dialogOverlay = new DialogOverlay(), }); Dependencies.Cache(manager); @@ -134,6 +134,27 @@ namespace osu.Game.Tests.Visual.Collections assertCollectionName(0, "2"); } + [Test] + public void TestCollectionNameCollisions() + { + AddStep("add dropdown", () => + { + Add(new CollectionFilterDropdown + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, + } + ); + }); + AddStep("add two collections with same name", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } }, + })); + } + [Test] public void TestRemoveCollectionViaButton() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs new file mode 100644 index 0000000000..fd9c09fd5f --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneBlueprintSelection : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private BlueprintContainer blueprintContainer + => Editor.ChildrenOfType<BlueprintContainer>().First(); + + [Test] + public void TestSelectedObjectHasPriorityWhenOverlapping() + { + var firstSlider = new Slider + { + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2()), + new PathControlPoint(new Vector2(150, -50)), + new PathControlPoint(new Vector2(300, 0)) + }), + Position = new Vector2(0, 100) + }; + var secondSlider = new Slider + { + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2()), + new PathControlPoint(new Vector2(-50, 50)), + new PathControlPoint(new Vector2(-100, 100)) + }), + Position = new Vector2(200, 0) + }; + + AddStep("add overlapping sliders", () => + { + EditorBeatmap.Add(firstSlider); + EditorBeatmap.Add(secondSlider); + }); + AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider)); + + AddStep("move mouse to common point", () => + { + var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre; + InputManager.MoveMouseTo(pos); + }); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + + AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs new file mode 100644 index 0000000000..390198be04 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit.Components; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + [TestFixture] + public class TestSceneEditorClock : EditorClockTestScene + { + public TestSceneEditorClock() + { + Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TimeInfoContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 100) + }, + new PlaybackControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 100) + } + } + }); + } + + [BackgroundDependencyLoader] + private void load() + { + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + // ensure that music controller does not change this beatmap due to it + // completing naturally as part of the test. + Beatmap.Disabled = true; + } + + [Test] + public void TestStopAtTrackEnd() + { + AddStep("reset clock", () => Clock.Seek(0)); + + AddStep("start clock", Clock.Start); + AddAssert("clock running", () => Clock.IsRunning); + + AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); + AddUntilStep("clock stops", () => !Clock.IsRunning); + + AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); + + AddStep("start clock again", Clock.Start); + AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); + } + + [Test] + public void TestWrapWhenStoppedAtTrackEnd() + { + AddStep("reset clock", () => Clock.Seek(0)); + + AddStep("stop clock", Clock.Stop); + AddAssert("clock stopped", () => !Clock.IsRunning); + + AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); + AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); + + AddStep("start clock again", Clock.Start); + AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index d6db171cf0..1da6433707 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -110,8 +110,6 @@ namespace osu.Game.Tests.Visual.Editing [Resolved] private EditorClock editorClock { get; set; } - private bool started; - public StartStopButton() { BackgroundColour = Color4.SlateGray; @@ -123,18 +121,17 @@ namespace osu.Game.Tests.Visual.Editing private void onClick() { - if (started) - { + if (editorClock.IsRunning) editorClock.Stop(); - Text = "Start"; - } else - { editorClock.Start(); - Text = "Stop"; - } + } - started = !started; + protected override void Update() + { + base.Update(); + + Text = editorClock.IsRunning ? "Stop" : "Start"; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 1c55595c97..5a1a9d3d87 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("show health", () => showHealth.Value = true); - AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddStep("enable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer is visible", () => layer.IsPresent); } @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestLayerDisabledViaConfig() { - AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -81,19 +81,19 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddStep("don't show health", () => showHealth.Value = false); - AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); AddStep("don't show health", () => showHealth.Value = false); - AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddStep("enable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); AddStep("show health", () => showHealth.Value = true); - AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); AddStep("show health", () => showHealth.Value = true); - AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddStep("enable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer fade is visible", () => layer.IsPresent); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index f9914e0193..3cefb8623f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode)); - AddStep("set hud to never show", () => config.Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); + AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); AddUntilStep("wait for fade", () => !hideTarget.IsPresent); @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft)); AddUntilStep("wait for fade", () => !hideTarget.IsPresent); - AddStep("set original config value", () => config.Set(OsuSetting.HUDVisibilityMode, originalConfigValue)); + AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue)); } [Test] @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set keycounter visible false", () => { - config.Set<bool>(OsuSetting.KeyOverlay, false); + config.SetValue(OsuSetting.KeyOverlay, false); hudOverlay.KeyCounter.AlwaysVisible.Value = false; }); @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); - AddStep("return value", () => config.Set<bool>(OsuSetting.KeyOverlay, keyCounterVisibleValue)); + AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue)); } private void createNew(Action<HUDOverlay> action = null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 563d6be0da..dccde366c2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(0, 0)] [TestCase(-1000, -1000)] [TestCase(-10000, -10000)] - public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime) + public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime) { var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); storyboard.GetLayer("Background").Add(sprite); @@ -64,6 +65,43 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [TestCase(1000, 0, false)] + [TestCase(0, 0, false)] + [TestCase(-1000, -1000, false)] + [TestCase(-10000, -10000, false)] + [TestCase(1000, 0, true)] + [TestCase(0, 0, true)] + [TestCase(-1000, -1000, true)] + [TestCase(-10000, -10000, true)] + public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop) + { + var storyboard = new Storyboard(); + + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + + // these should be ignored as we have an alpha visibility blocker proceeding this command. + sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + var loopGroup = sprite.AddLoop(-20000, 50); + loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + + var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; + target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); + + // these should be ignored due to being in the future. + sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); + loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); + + storyboard.GetLayer("Background").Add(sprite); + + loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); + + AddAssert($"first frame is {expectedStartTime}", () => + { + Debug.Assert(player.FirstFrameClockTime != null); + return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); + }); + } + private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { AddStep("create player", () => diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index e43e5ba3ce..49c1163c6c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Framework.Allocation; using osu.Framework.Bindables; @@ -8,21 +9,17 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osuTK; namespace osu.Game.Tests.Visual.Gameplay { [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. public class TestScenePauseWhenInactive : OsuPlayerTestScene { - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) - { - var beatmap = (Beatmap)base.CreateBeatmap(ruleset); - - beatmap.HitObjects.RemoveAll(h => h.StartTime < 30000); - - return beatmap; - } - [Resolved] private GameHost host { get; set; } @@ -33,10 +30,57 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("resume player", () => Player.GameplayClockContainer.Start()); AddAssert("ensure not paused", () => !Player.GameplayClockContainer.IsPaused.Value); + + AddStep("progress time to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime)); + AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); + } + + /// <summary> + /// Tests that if a pause from focus lose is performed while in pause cooldown, + /// the player will still pause after the cooldown is finished. + /// </summary> + [Test] + public void TestPauseWhileInCooldown() + { + AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); + + AddStep("resume player", () => Player.GameplayClockContainer.Start()); + AddStep("skip to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime)); + + AddStep("set inactive", () => ((Bindable<bool>)host.IsActive).Value = false); + AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); + + AddStep("set active", () => ((Bindable<bool>)host.IsActive).Value = true); + + AddStep("resume player", () => Player.Resume()); + AddAssert("unpaused", () => !Player.GameplayClockContainer.IsPaused.Value); + + bool pauseCooldownActive = false; + + AddStep("set inactive again", () => + { + pauseCooldownActive = Player.PauseCooldownActive; + ((Bindable<bool>)host.IsActive).Value = false; + }); + AddAssert("pause cooldown active", () => pauseCooldownActive); AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); - AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime); } protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + return new Beatmap + { + HitObjects = new List<HitObject> + { + new HitCircle { StartTime = 30000 }, + new HitCircle { StartTime = 35000 }, + }, + }; + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => new TestWorkingBeatmap(beatmap, storyboard, Audio); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 44142b69d7..7a6e2f54c2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); - public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); } @@ -309,7 +309,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); - public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); } @@ -321,7 +321,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); - public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index d688e9cb21..d792405eeb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); - public Sample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); + public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup); public void TriggerSourceChanged() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 1544f8fd35..a718a98aa6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - config.Set(OsuSetting.ShowStoryboard, true); + config.SetValue(OsuSetting.ShowStoryboard, true); storyboard = new Storyboard(); var backgroundLayer = storyboard.GetLayer("Background"); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 2e39471dc0..78bc51e47b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1,46 +1,41 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayer : MultiplayerTestScene + public class TestSceneMultiplayer : ScreenTestScene { + private TestMultiplayer multiplayerScreen; + public TestSceneMultiplayer() { - var multi = new TestMultiplayer(); + AddStep("show", () => + { + multiplayerScreen = new TestMultiplayer(); - AddStep("show", () => LoadScreen(multi)); - AddUntilStep("wait for loaded", () => multi.IsLoaded); - } + // Needs to be added at a higher level since the multiplayer screen becomes non-current. + Child = multiplayerScreen.Client; - [Test] - public void TestOneUserJoinedMultipleTimes() - { - var user = new User { Id = 33 }; + LoadScreen(multiplayerScreen); + }); - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - } - - [Test] - public void TestOneUserLeftMultipleTimes() - { - var user = new User { Id = 44 }; - - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded); } private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer { + [Cached(typeof(StatefulMultiplayerClient))] + public readonly TestMultiplayerClient Client; + + public TestMultiplayer() + { + Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager); + } + protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 95c333e9f4..faa5d9e6fc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -7,17 +7,23 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Select; @@ -137,8 +143,30 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime); } + [TestCase(typeof(OsuModHidden), typeof(OsuModHidden))] // Same mod. + [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. + public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) + { + AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) }); + AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); + + AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0); + assertHasFreeModButton(allowedMod, false); + assertHasFreeModButton(requiredMod, false); + } + + private void assertHasFreeModButton(Type type, bool hasButton = true) + { + AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", + () => songSelect.ChildrenOfType<FreeModSelectOverlay>().Single().ChildrenOfType<ModButton>().All(b => b.Mod.GetType() != type)); + } + private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect { + public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods; + + public new Bindable<IReadOnlyList<Mod>> FreeMods => base.FreeMods; + public new BeatmapCarousel Carousel => base.Carousel; } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 2344ebea0e..8869718fd1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -3,12 +3,10 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; @@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerMatchSubScreen screen; - [Cached] - private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - public TestSceneMultiplayerMatchSubScreen() : base(false) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 3b3b1bee86..b44e5b1e5b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); - beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); Add(beatmapTracker = new OnlinePlayBeatmapAvailablilityTracker { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 6de5704410..91c15de69f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { @@ -21,15 +24,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room { Name = { Value = "1" } }); + roomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); roomManager.PartRoom(); - roomManager.CreateRoom(new Room { Name = { Value = "2" } }); + roomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); roomManager.PartRoom(); roomManager.ClearRooms(); }); }); - AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); } @@ -40,16 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); AddStep("disconnect", () => roomContainer.Client.Disconnect()); - AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0); + AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0); AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } @@ -60,9 +63,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); @@ -70,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("disconnect", () => roomContainer.Client.Disconnect()); AddStep("connect", () => roomContainer.Client.Connect()); - AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); } @@ -81,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.ClearRooms(); }); }); - AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0); + AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0); AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } @@ -97,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); }); }); @@ -111,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); @@ -126,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - var r = new Room(); + var r = createRoom(); roomManager.CreateRoom(r); roomManager.PartRoom(); roomManager.JoinRoom(r); @@ -136,6 +139,21 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); } + private Room createRoom(Action<Room> initFunc = null) + { + var room = new Room(); + + room.Name.Value = "test room"; + room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }); + + initFunc?.Invoke(room); + return room; + } + private TestMultiplayerRoomManager createRoomManager() { Child = roomContainer = new TestMultiplayerRoomContainer diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 96393cc4c3..bf5338d81a 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; @@ -62,10 +61,6 @@ namespace osu.Game.Tests.Visual.Navigation RecycleLocalStorage(); - // see MouseSettings - var frameworkConfig = host.Dependencies.Get<FrameworkConfigManager>(); - frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity).Disabled = false; - CreateGame(); }); @@ -82,7 +77,7 @@ namespace osu.Game.Tests.Visual.Navigation // todo: this can be removed once we can run audio tracks without a device present // see https://github.com/ppy/osu/issues/1302 - Game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles); + Game.LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles); Add(Game); } @@ -136,7 +131,7 @@ namespace osu.Game.Tests.Visual.Navigation base.LoadComplete(); API.Login("Rhythm Champion", "osu!"); - Dependencies.Get<SessionStatics>().Set(Static.MutedAudioNotificationShownOnce, true); + Dependencies.Get<SessionStatics>().SetValue(Static.MutedAudioNotificationShownOnce, true); } } diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 21d3bdaae3..2791952b66 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestPerformAtSongSelectFromPlayerLoader() { PushAndConfirm(() => new PlaySongSelect()); - PushAndConfirm(() => new PlayerLoader(() => new Player())); + PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) })); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestPerformAtMenuFromPlayerLoader() { PushAndConfirm(() => new PlaySongSelect()); - PushAndConfirm(() => new PlayerLoader(() => new Player())); + PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d8380b2dd3..f2bb518b2e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => new TestSongSelect()); - AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); - AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length)); + AddStep("seek to end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Track.Length)); AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action()); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); @@ -214,6 +214,50 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible); } + [Test] + public void TestSettingsViaHotkeyFromMainMenu() + { + AddAssert("toolbar not displayed", () => Game.Toolbar.State.Value == Visibility.Hidden); + + AddStep("press settings hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.O); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("settings displayed", () => Game.Settings.State.Value == Visibility.Visible); + } + + [Test] + public void TestToolbarHiddenByUser() + { + AddStep("Enter menu", () => InputManager.Key(Key.Enter)); + + AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); + + AddStep("Hide toolbar", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.T); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + pushEscape(); + + AddStep("Enter menu", () => InputManager.Key(Key.Enter)); + + AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); + + AddStep("Enter song select", () => + { + InputManager.Key(Key.Enter); + InputManager.Key(Key.Enter); + }); + + AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); + } + private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs index c0b77b580e..c1c968e862 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -15,8 +15,8 @@ namespace osu.Game.Tests.Visual.Navigation using (var config = new OsuConfigManager(LocalStorage)) { - config.Set(OsuSetting.Version, "2020.101.0"); - config.Set(OsuSetting.DisplayStarsMaximum, 10.0); + config.SetValue(OsuSetting.Version, "2020.101.0"); + config.SetValue(OsuSetting.DisplayStarsMaximum, 10.0); } } @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddAssert("config has migrated value", () => Precision.AlmostEquals(Game.LocalConfig.Get<double>(OsuSetting.DisplayStarsMaximum), 10.1)); - AddStep("set value again", () => Game.LocalConfig.Set<double>(OsuSetting.DisplayStarsMaximum, 10)); + AddStep("set value again", () => Game.LocalConfig.SetValue(OsuSetting.DisplayStarsMaximum, 10.0)); AddStep("force save config", () => Game.LocalConfig.Save()); diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 1349264bf9..156d6b744e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -30,13 +30,14 @@ namespace osu.Game.Tests.Visual.Online ((DummyAPIAccess)API).HandleRequest = req => { - if (req is SearchBeatmapSetsRequest searchBeatmapSetsRequest) + if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false; + + searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse { - searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse - { - BeatmapSets = setsForResponse, - }); - } + BeatmapSets = setsForResponse, + }); + + return true; }; } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index cd2c4e9346..8818ac75b1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -63,13 +63,15 @@ namespace osu.Game.Tests.Visual.Online Builds = builds.Values.ToList() }; changelogRequest.TriggerSuccess(changelogResponse); - break; + return true; case GetChangelogBuildRequest buildRequest: if (requestedBuild != null) buildRequest.TriggerSuccess(requestedBuild); - break; + return true; } + + return false; }; Child = changelog = new TestChangelogOverlay(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index fca642ad6c..b13dd34ebc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat.Selection; @@ -64,6 +66,24 @@ namespace osu.Game.Tests.Visual.Online }); } + [SetUpSteps] + public void SetUpSteps() + { + AddStep("register request handling", () => + { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case JoinChannelRequest _: + return true; + } + + return false; + }; + }); + } + [Test] public void TestHideOverlay() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index c2a18330c9..cd22bb2513 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -85,9 +85,10 @@ namespace osu.Game.Tests.Visual.Online dummyAPI.HandleRequest = request => { if (!(request is GetCommentsRequest getCommentsRequest)) - return; + return false; getCommentsRequest.TriggerSuccess(commentBundle); + return true; }; }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index 63bda08c88..0c199bfb62 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online ensureSoleilyRemoved(); createButtonWithBeatmap(createSoleily()); AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded); - AddStep("import soleily", () => beatmaps.Import(TestResources.GetTestBeatmapForImport())); + AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport())); AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526)); createButtonWithBeatmap(createSoleily()); AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable); diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 9bece39ca0..e8d9ff72af 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Online Username = "flyte", Id = 3103765, IsOnline = true, - CurrentModeRank = 1111, + Statistics = new UserStatistics { GlobalRank = 1111 }, Country = new Country { FlagName = "JP" }, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }, @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online Username = "peppy", Id = 2, IsOnline = false, - CurrentModeRank = 2222, + Statistics = new UserStatistics { GlobalRank = 2222 }, Country = new Country { FlagName = "AU" }, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", IsSupporter = true, diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 37d51c16d2..6ebe8fcc07 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -33,9 +33,10 @@ namespace osu.Game.Tests.Visual.Online dummyAPI.HandleRequest = request => { if (!(request is GetNewsRequest getNewsRequest)) - return; + return false; getNewsRequest.TriggerSuccess(r); + return true; }; }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index 8dd81e02e2..255f147ec9 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Playlists Room.RecentParticipants.Add(new User { Username = "peppy", - CurrentModeRank = 1234, + Statistics = new UserStatistics { GlobalRank = 1234 }, Id = 2 }); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index cdcded8f61..61d49e4018 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; -using System.Threading.Tasks; using JetBrains.Annotations; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -76,7 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; - bindHandler(3000, userScore); + bindHandler(true, userScore); }); createResults(() => userScore); @@ -89,7 +88,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScoreWithDelay() { - AddStep("bind delayed handler", () => bindHandler(3000)); + AddStep("bind delayed handler", () => bindHandler(true)); createResults(); waitForDisplay(); @@ -103,7 +102,7 @@ namespace osu.Game.Tests.Visual.Playlists createResults(); waitForDisplay(); - AddStep("bind delayed handler", () => bindHandler(3000)); + AddStep("bind delayed handler", () => bindHandler(true)); for (int i = 0; i < 2; i++) { @@ -134,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists createResults(() => userScore); waitForDisplay(); - AddStep("bind delayed handler", () => bindHandler(3000)); + AddStep("bind delayed handler", () => bindHandler(true)); for (int i = 0; i < 2; i++) { @@ -169,70 +168,60 @@ namespace osu.Game.Tests.Visual.Playlists AddWaitStep("wait for display", 5); } - private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => + private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => { - requestComplete = false; - - if (failRequests) - { - triggerFail(request, delay); - return; - } - + // pre-check for requests we should be handling (as they are scheduled below). switch (request) { - case ShowPlaylistUserScoreRequest s: - if (userScore == null) - triggerFail(s, delay); - else - triggerSuccess(s, createUserResponse(userScore), delay); + case ShowPlaylistUserScoreRequest _: + case IndexPlaylistScoresRequest _: break; - case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i), delay); - break; + default: + return false; } + + requestComplete = false; + + double delay = delayed ? 3000 : 0; + + Scheduler.AddDelayed(() => + { + if (failRequests) + { + triggerFail(request); + return; + } + + switch (request) + { + case ShowPlaylistUserScoreRequest s: + if (userScore == null) + triggerFail(s); + else + triggerSuccess(s, createUserResponse(userScore)); + break; + + case IndexPlaylistScoresRequest i: + triggerSuccess(i, createIndexResponse(i)); + break; + } + }, delay); + + return true; }; - private void triggerSuccess<T>(APIRequest<T> req, T result, double delay) + private void triggerSuccess<T>(APIRequest<T> req, T result) where T : class { - if (delay == 0) - success(); - else - { - Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMilliseconds(delay)); - Schedule(success); - }); - } - - void success() - { - requestComplete = true; - req.TriggerSuccess(result); - } + requestComplete = true; + req.TriggerSuccess(result); } - private void triggerFail(APIRequest req, double delay) + private void triggerFail(APIRequest req) { - if (delay == 0) - fail(); - else - { - Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMilliseconds(delay)); - Schedule(fail); - }); - } - - void fail() - { - requestComplete = true; - req.TriggerFailure(new WebException("Failed.")); - } + requestComplete = true; + req.TriggerFailure(new WebException("Failed.")); } private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 7be44a62de..591095252f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Ranking Beatmap = createTestBeatmap(author) })); - AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Text == "mapper_name")); + AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Current.Value == "mapper_name")); } [Test] @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking })); AddAssert("mapped by text not present", () => - this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); + this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); } private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 8330b9b360..f495e0fb23 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().Text.Text)); + AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().Text.Text.ToString())); AddStep("click second binding", () => { @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1).Text.Text)); + AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1).Text.Text.ToString())); void clickClearButton() { diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs new file mode 100644 index 0000000000..8f1c17ed29 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Tests.Visual.Settings +{ + [TestFixture] + public class TestSceneSettingsItem : OsuTestScene + { + [Test] + public void TestRestoreDefaultValueButtonVisibility() + { + TestSettingsTextBox textBox = null; + + AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox + { + Current = new Bindable<string> + { + Default = "test", + Value = "test" + } + }); + AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0); + + AddStep("change value from default", () => textBox.Current.Value = "non-default"); + AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0); + + AddStep("restore default", () => textBox.Current.SetDefault()); + AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0); + } + + private class TestSettingsTextBox : SettingsTextBox + { + public new Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton>().Single(); + } + } +} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs new file mode 100644 index 0000000000..a7f6c8c0d3 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Handlers.Tablet; +using osu.Framework.Platform; +using osu.Framework.Utils; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings.Sections.Input; +using osuTK; + +namespace osu.Game.Tests.Visual.Settings +{ + [TestFixture] + public class TestSceneTabletSettings : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(GameHost host) + { + var tabletHandler = new TestTabletHandler(); + + AddRange(new Drawable[] + { + new TabletSettings(tabletHandler) + { + RelativeSizeAxes = Axes.None, + Width = SettingsPanel.WIDTH, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + }); + + AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100))); + AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300))); + AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300))); + AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 700))); + AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero)); + } + + public class TestTabletHandler : ITabletHandler + { + public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>(); + public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>(); + + public IBindable<TabletInfo> Tablet => tablet; + + private readonly Bindable<TabletInfo> tablet = new Bindable<TabletInfo>(); + + public BindableBool Enabled { get; } = new BindableBool(true); + + public void SetTabletSize(Vector2 size) + { + tablet.Value = size != Vector2.Zero ? new TabletInfo($"test tablet T-{RNG.Next(999):000}", size) : null; + + AreaSize.Default = new Vector2(size.X, size.Y); + + // if it's clear the user has not configured the area, take the full area from the tablet that was just found. + if (AreaSize.Value == Vector2.Zero) + AreaSize.SetDefault(); + + AreaOffset.Default = new Vector2(size.X / 2, size.Y / 2); + + // likewise with the position, use the centre point if it has not been configured. + // it's safe to assume no user would set their centre point to 0,0 for now. + if (AreaOffset.Value == Vector2.Zero) + AreaOffset.SetDefault(); + } + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 0b2c0ce63b..7ea6373763 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; @@ -103,27 +104,27 @@ namespace osu.Game.Tests.Visual.SongSelect private void testBeatmapLabels(Ruleset ruleset) { - AddAssert("check version", () => infoWedge.Info.VersionLabel.Text == $"{ruleset.ShortName}Version"); - AddAssert("check title", () => infoWedge.Info.TitleLabel.Text == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); - AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Text == $"{ruleset.ShortName}Artist"); - AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType<OsuSpriteText>().Any(s => s.Text == $"{ruleset.ShortName}Author")); + AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version"); + AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); + AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); + AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author")); } private void testInfoLabels(int expectedCount) { - AddAssert("check info labels exists", () => infoWedge.Info.InfoLabelContainer.Children.Any()); - AddAssert("check info labels count", () => infoWedge.Info.InfoLabelContainer.Children.Count == expectedCount); + AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any()); + AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount); } [Test] public void TestNullBeatmap() { selectBeatmap(null); - AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text)); - AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title); - AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Current.Value)); + AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); - AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); + AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any()); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 53a956c77c..5e2d5eba5d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -32,8 +32,10 @@ namespace osu.Game.Tests.Visual.SongSelect { case GetUserRequest userRequest: userRequest.TriggerSuccess(getUser(userRequest.Ruleset.ID)); - break; + return true; } + + return false; }; }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 5d0fb248df..c13bdf0955 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); - beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); base.Content.AddRange(new Drawable[] { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 35c6d62cb7..5731b1ac2c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -207,14 +207,14 @@ namespace osu.Game.Tests.Visual.SongSelect addRulesetImportStep(0); addRulesetImportStep(0); - AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false)); + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); createSongSelect(); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); - AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); AddStep("return", () => songSelect.MakeCurrent()); AddUntilStep("wait for current", () => songSelect.IsCurrentScreen()); @@ -297,13 +297,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); - AddStep(@"Sort by Artist", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Artist)); - AddStep(@"Sort by Title", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Title)); - AddStep(@"Sort by Author", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Author)); - AddStep(@"Sort by DateAdded", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.DateAdded)); - AddStep(@"Sort by BPM", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.BPM)); - AddStep(@"Sort by Length", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Length)); - AddStep(@"Sort by Difficulty", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); + AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); + AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title)); + AddStep(@"Sort by Author", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author)); + AddStep(@"Sort by DateAdded", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded)); + AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM)); + AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length)); + AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); } [Test] @@ -470,7 +470,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(0); // used for filter check below - AddStep("allow convert display", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); @@ -648,7 +648,7 @@ namespace osu.Game.Tests.Visual.SongSelect { int changeCount = 0; - AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false)); + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); AddStep("bind beatmap changed", () => { Beatmap.ValueChanged += onChange; @@ -686,7 +686,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("selection changed only fired twice", () => changeCount == 2); AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange); - AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); // ReSharper disable once AccessToModifiedClosure void onChange(ValueChangedEvent<WorkingBeatmap> valueChangedEvent) => changeCount++; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index a9747e73f9..abd1baf0ac 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -34,6 +34,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void SetUp() => Schedule(() => { OsuSpriteText query; + OsuSpriteText general; OsuSpriteText ruleset; OsuSpriteText category; OsuSpriteText genre; @@ -58,6 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface Children = new Drawable[] { query = new OsuSpriteText(), + general = new OsuSpriteText(), ruleset = new OsuSpriteText(), category = new OsuSpriteText(), genre = new OsuSpriteText(), @@ -71,6 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); + control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); @@ -92,10 +95,10 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestExplicitConfig() { - AddStep("configure explicit content to allowed", () => localConfig.Set(OsuSetting.ShowOnlineExplicitContent, true)); + AddStep("configure explicit content to allowed", () => localConfig.SetValue(OsuSetting.ShowOnlineExplicitContent, true)); AddAssert("explicit control set to show", () => control.ExplicitContent.Value == SearchExplicit.Show); - AddStep("configure explicit content to disallowed", () => localConfig.Set(OsuSetting.ShowOnlineExplicitContent, false)); + AddStep("configure explicit content to disallowed", () => localConfig.SetValue(OsuSetting.ShowOnlineExplicitContent, false)); AddAssert("explicit control set to hide", () => control.ExplicitContent.Value == SearchExplicit.Hide); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 81862448a8..97f3b2954d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result.Beatmaps[0]; + beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; for (int i = 0; i < 50; i++) { @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click delete option", () => { - InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.Value.ToLowerInvariant() == "delete")); + InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.Value.ToString().ToLowerInvariant() == "delete")); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 1e3b1c2ffd..546e905ded 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.UserInterface var multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); var expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - return expectedValue == footerButtonMods.MultiplierText.Text; + return expectedValue == footerButtonMods.MultiplierText.Current.Value; } private class TestFooterButtonMods : FooterButtonMods diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs index 45720548c8..493e2f54e5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs @@ -44,22 +44,22 @@ namespace osu.Game.Tests.Visual.UserInterface protected override void InitialiseDefaults() { - Set(TestConfigSetting.ToggleSettingNoKeybind, false); - Set(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1); - Set(TestConfigSetting.ToggleSettingWithKeybind, false); - Set(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1); + SetDefault(TestConfigSetting.ToggleSettingNoKeybind, false); + SetDefault(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1); + SetDefault(TestConfigSetting.ToggleSettingWithKeybind, false); + SetDefault(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1); base.InitialiseDefaults(); } - public void ToggleSetting(TestConfigSetting setting) => Set(setting, !Get<bool>(setting)); + public void ToggleSetting(TestConfigSetting setting) => SetValue(setting, !Get<bool>(setting)); public void IncrementEnumSetting(TestConfigSetting setting) { var nextValue = Get<EnumSetting>(setting) + 1; if (nextValue > EnumSetting.Setting4) nextValue = EnumSetting.Setting1; - Set(setting, nextValue); + SetValue(setting, nextValue); } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 7e3868bd3b..e36b3cdc74 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,16 +3,19 @@ <ItemGroup Label="Package References"> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> - <PackageReference Include="Moq" Version="4.16.0" /> + <PackageReference Include="Moq" Version="4.16.1" /> </ItemGroup> <PropertyGroup Label="Project"> <OutputType>WinExe</OutputType> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> + <PropertyGroup Label="Code Analysis"> + <CodeAnalysisRuleSet>tests.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> <ItemGroup Label="Project References"> <ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" /> diff --git a/osu.Game.Tests/tests.ruleset b/osu.Game.Tests/tests.ruleset new file mode 100644 index 0000000000..a0abb781d3 --- /dev/null +++ b/osu.Game.Tests/tests.ruleset @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<RuleSet Name="osu! Rule Set" Description=" " ToolsVersion="16.0"> + <Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers"> + <Rule Id="CA2007" Action="None" /> + </Rules> +</RuleSet> diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 567d9f0d62..46c3b8bc3b 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Tests.NonVisual storage.DeleteDirectory(string.Empty); using (var storageConfig = new TournamentStorageManager(storage)) - storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament); + storageConfig.SetValue(StorageConfig.CurrentTournament, custom_tournament); try { diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs new file mode 100644 index 0000000000..4791da93c6 --- /dev/null +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Tournament.IO; +using osu.Game.Tournament.IPC; + +namespace osu.Game.Tournament.Tests.NonVisual +{ + [TestFixture] + public class IPCLocationTest + { + [Test] + public void CheckIPCLocation() + { + // don't use clean run because files are being written before osu! launches. + using (HeadlessGameHost host = new HeadlessGameHost(nameof(CheckIPCLocation))) + { + string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(CheckIPCLocation)); + + // Set up a fake IPC client for the IPC Storage to switch to. + string testCeDir = Path.Combine(basePath, "stable-ce"); + Directory.CreateDirectory(testCeDir); + + string ipcFile = Path.Combine(testCeDir, "ipc.txt"); + File.WriteAllText(ipcFile, string.Empty); + + try + { + var osu = loadOsu(host); + TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>(); + FileBasedIPC ipc = (FileBasedIPC)osu.Dependencies.Get<MatchIPCInfo>(); + + waitForOrAssert(() => ipc != null, @"ipc could not be populated in a reasonable amount of time"); + + Assert.True(ipc.SetIPCLocation(testCeDir)); + Assert.True(storage.AllTournaments.Exists("stable.json")); + } + finally + { + host.Storage.DeleteDirectory(testCeDir); + host.Storage.DeleteDirectory("tournaments"); + host.Exit(); + } + } + } + + private TournamentGameBase loadOsu(GameHost host) + { + var osu = new TournamentGameBase(); + Task.Run(() => host.Run(osu)); + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + return osu; + } + + private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + } +} diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs new file mode 100644 index 0000000000..e2954c8f10 --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Graphics.Cursor; +using osu.Game.Tournament.Screens.Drawings; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneDrawingsScreen : TournamentTestScene + { + [BackgroundDependencyLoader] + private void load(Storage storage) + { + using (var stream = storage.GetStream("drawings.txt", FileAccess.Write)) + using (var writer = new StreamWriter(stream)) + { + writer.WriteLine("KR : South Korea : KOR"); + writer.WriteLine("US : United States : USA"); + writer.WriteLine("PH : Philippines : PHL"); + writer.WriteLine("BR : Brazil : BRA"); + writer.WriteLine("JP : Japan : JPN"); + } + + Add(new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new DrawingsScreen() + }); + } + } +} diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 77ae06d89c..b20583dd7e 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ </PropertyGroup> <ItemGroup Label="Package References"> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> </ItemGroup> diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index d1197b1a61..e6d73c6e83 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -74,9 +74,9 @@ namespace osu.Game.Tournament.Components { new TournamentSpriteText { - Text = new LocalisedString(( + Text = new RomanisableString( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), + $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 2ba1b6be8f..044b60bbd5 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.IO; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; -using System.IO; -using System.Collections.Generic; using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament.IO @@ -15,7 +15,12 @@ namespace osu.Game.Tournament.IO { private const string default_tournament = "default"; private readonly Storage storage; - private readonly Storage allTournaments; + + /// <summary> + /// The storage where all tournaments are located. + /// </summary> + public readonly Storage AllTournaments; + private readonly TournamentStorageManager storageConfig; public readonly Bindable<string> CurrentTournament; @@ -23,16 +28,16 @@ namespace osu.Game.Tournament.IO : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { this.storage = storage; - allTournaments = UnderlyingStorage; + AllTournaments = UnderlyingStorage; storageConfig = new TournamentStorageManager(storage); if (storage.Exists("tournament.ini")) { - ChangeTargetStorage(allTournaments.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament))); + ChangeTargetStorage(AllTournaments.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament))); } else - Migrate(allTournaments.GetStorageForDirectory(default_tournament)); + Migrate(AllTournaments.GetStorageForDirectory(default_tournament)); CurrentTournament = storageConfig.GetBindable<string>(StorageConfig.CurrentTournament); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); @@ -42,11 +47,11 @@ namespace osu.Game.Tournament.IO private void updateTournament(ValueChangedEvent<string> newTournament) { - ChangeTargetStorage(allTournaments.GetStorageForDirectory(newTournament.NewValue)); + ChangeTargetStorage(AllTournaments.GetStorageForDirectory(newTournament.NewValue)); Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty)); } - public IEnumerable<string> ListTournaments() => allTournaments.GetDirectories(string.Empty); + public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty); public override void Migrate(Storage newStorage) { @@ -70,7 +75,7 @@ namespace osu.Game.Tournament.IO moveFileIfExists("drawings.ini", destination); ChangeTargetStorage(newStorage); - storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); + storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); } diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 0b0050a245..d390f88d59 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -5,6 +5,7 @@ using System; using System.IO; using Newtonsoft.Json; using osu.Framework.Platform; +using osu.Game.Tournament.IO; namespace osu.Game.Tournament.Models { @@ -24,13 +25,14 @@ namespace osu.Game.Tournament.Models /// </summary> public event Action OnStableInfoSaved; - private const string config_path = "tournament/stable.json"; + private const string config_path = "stable.json"; private readonly Storage storage; public StableInfo(Storage storage) { - this.storage = storage; + TournamentStorage tStorage = (TournamentStorage)storage; + this.storage = tStorage.AllTournaments; if (!storage.Exists(config_path)) return; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs index d197c0f5d9..1a2f5a1ff4 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs @@ -12,8 +12,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components protected override void InitialiseDefaults() { - Set(DrawingsConfig.Groups, 8, 1, 8); - Set(DrawingsConfig.TeamsPerGroup, 8, 1, 8); + SetDefault(DrawingsConfig.Groups, 8, 1, 8); + SetDefault(DrawingsConfig.TeamsPerGroup, 8, 1, 8); } public DrawingsConfigManager(Storage storage) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs index 3ff4718b75..c7060bd538 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs @@ -345,7 +345,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components Flag.Anchor = Anchor.Centre; Flag.Origin = Anchor.Centre; - Flag.Scale = new Vector2(0.9f); + Flag.Scale = new Vector2(0.7f); InternalChildren = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 582f72429b..aa1be143ea 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using Newtonsoft.Json; @@ -43,10 +44,13 @@ namespace osu.Game.Tournament.Screens.Editors private void addAllCountries() { List<TournamentTeam> countries; + using (Stream stream = game.Resources.GetStream("Resources/countries.json")) using (var sr = new StreamReader(stream)) countries = JsonConvert.DeserializeObject<List<TournamentTeam>>(sr.ReadToEnd()); + Debug.Assert(countries != null); + foreach (var c in countries) Storage.Add(c); } diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index fadb821bef..87e23e3404 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -2,18 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System.Drawing; -using osu.Framework.Extensions.Color4Extensions; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Colour; -using osu.Game.Graphics.Cursor; -using osu.Game.Tournament.Models; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Platform; using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; +using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -36,7 +39,7 @@ namespace osu.Game.Tournament private LoadingSpinner loadingSpinner; [BackgroundDependencyLoader] - private void load(FrameworkConfigManager frameworkConfig) + private void load(FrameworkConfigManager frameworkConfig, GameHost host) { windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize); windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode); @@ -48,6 +51,13 @@ namespace osu.Game.Tournament Margin = new MarginPadding(40), }); + // in order to have the OS mouse cursor visible, relative mode needs to be disabled. + // can potentially be removed when https://github.com/ppy/osu-framework/issues/4309 is resolved. + var mouseHandler = host.AvailableInputHandlers.OfType<MouseHandler>().FirstOrDefault(); + + if (mouseHandler != null) + mouseHandler.UseRelativeMode.Value = false; + loadingSpinner.Show(); BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[] diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index d506724017..2ee52c35aa 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -150,7 +150,9 @@ namespace osu.Game.Tournament { foreach (var p in t.Players) { - if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null) + if (string.IsNullOrEmpty(p.Username) + || p.Statistics?.GlobalRank == null + || p.Statistics?.CountryRank == null) { PopulateUser(p, immediate: true); addedInfo = true; diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index cb0b3a8d09..b291edd19d 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -17,12 +17,12 @@ namespace osu.Game.Beatmaps public abstract class BeatmapConverter<T> : IBeatmapConverter where T : HitObject { - private event Action<HitObject, IEnumerable<HitObject>> ObjectConverted; + private event Action<HitObject, IEnumerable<HitObject>> objectConverted; event Action<HitObject, IEnumerable<HitObject>> IBeatmapConverter.ObjectConverted { - add => ObjectConverted += value; - remove => ObjectConverted -= value; + add => objectConverted += value; + remove => objectConverted -= value; } public IBeatmap Beatmap { get; } @@ -92,10 +92,10 @@ namespace osu.Game.Beatmaps var converted = ConvertHitObject(obj, beatmap, cancellationToken); - if (ObjectConverted != null) + if (objectConverted != null) { converted = converted.ToList(); - ObjectConverted.Invoke(obj, converted); + objectConverted.Invoke(obj, converted); } foreach (var c in converted) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 37d262abe5..53d82c385d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -69,8 +69,8 @@ namespace osu.Game.Beatmaps /// </summary> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param> - /// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns> - public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + /// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated).</returns> + public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); @@ -90,9 +90,9 @@ namespace osu.Game.Beatmaps /// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param> /// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param> - /// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns> - public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, - CancellationToken cancellationToken = default) + /// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state.</returns> + public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, + CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); /// <summary> @@ -313,7 +313,7 @@ namespace osu.Game.Beatmaps } } - private class BindableStarDifficulty : Bindable<StarDifficulty> + private class BindableStarDifficulty : Bindable<StarDifficulty?> { public readonly BeatmapInfo Beatmap; public readonly CancellationToken CancellationToken; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 3c6a6ba302..b4ea898b7d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -20,6 +20,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; @@ -112,8 +113,6 @@ namespace osu.Game.Beatmaps { var metadata = new BeatmapMetadata { - Artist = "artist", - Title = "title", Author = user, }; @@ -127,7 +126,6 @@ namespace osu.Game.Beatmaps BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset, Metadata = metadata, - Version = "difficulty" } } }; @@ -155,7 +153,7 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); if (onlineLookupQueue != null) - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken); + await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) @@ -311,6 +309,9 @@ namespace osu.Game.Beatmaps workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); + // best effort; may be higher than expected. + GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); + return working; } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 0bc5605051..73337ab6f5 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// </summary> public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1) { - Precision = 0.1, + Precision = 0.01, Default = 1, MinValue = 0.1, MaxValue = 10 diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 96e18f120a..c62b803d1a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps.Drawables this.mods = mods; } - private IBindable<StarDifficulty> localStarDifficulty; + private IBindable<StarDifficulty?> localStarDifficulty; [BackgroundDependencyLoader] private void load() @@ -160,7 +160,11 @@ namespace osu.Game.Beatmaps.Drawables localStarDifficulty = ruleset != null ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token); - localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue); + localStarDifficulty.BindValueChanged(d => + { + if (d.NewValue is StarDifficulty diff) + StarDifficulty.Value = diff; + }); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 37ab489da5..40bc75e847 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; @@ -66,16 +67,14 @@ namespace osu.Game.Beatmaps.Formats protected override void ParseLine(Beatmap beatmap, Section section, string line) { - var strippedLine = StripComments(line); - switch (section) { case Section.General: - handleGeneral(strippedLine); + handleGeneral(line); return; case Section.Editor: - handleEditor(strippedLine); + handleEditor(line); return; case Section.Metadata: @@ -83,19 +82,19 @@ namespace osu.Game.Beatmaps.Formats return; case Section.Difficulty: - handleDifficulty(strippedLine); + handleDifficulty(line); return; case Section.Events: - handleEvent(strippedLine); + handleEvent(line); return; case Section.TimingPoints: - handleTimingPoint(strippedLine); + handleTimingPoint(line); return; case Section.HitObjects: - handleHitObject(strippedLine); + handleHitObject(line); return; } @@ -348,8 +347,8 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 8) { LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine); + kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine); } string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index df940e8c8e..d06478b9de 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -471,9 +471,6 @@ namespace osu.Game.Beatmaps.Formats private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) { - if (hitSampleInfo == null) - return "0"; - if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture); diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 2fb24c24e0..b39890084f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -36,6 +36,14 @@ namespace osu.Game.Beatmaps.Formats if (ShouldSkipLine(line)) continue; + if (section != Section.Metadata) + { + // comments should not be stripped from metadata lines, as the song metadata may contain "//" as valid data. + line = StripComments(line); + } + + line = line.TrimEnd(); + if (line.StartsWith('[') && line.EndsWith(']')) { if (!Enum.TryParse(line[1..^1], out section)) @@ -71,8 +79,6 @@ namespace osu.Game.Beatmaps.Formats protected virtual void ParseLine(T output, Section section, string line) { - line = StripComments(line); - switch (section) { case Section.Colours: diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index b9bf6823b5..6301c42deb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -45,8 +45,6 @@ namespace osu.Game.Beatmaps.Formats protected override void ParseLine(Storyboard storyboard, Section section, string line) { - line = StripComments(line); - switch (section) { case Section.General: diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index aab8ff6bd6..f7f276230f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; -using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -34,8 +33,6 @@ namespace osu.Game.Beatmaps protected AudioManager AudioManager { get; } - private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s"); - protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) { AudioManager = audioManager; @@ -47,8 +44,6 @@ namespace osu.Game.Beatmaps waveform = new RecyclableLazy<Waveform>(GetWaveform); storyboard = new RecyclableLazy<Storyboard>(GetStoryboard); skin = new RecyclableLazy<ISkin>(GetSkin); - - total_count.Value++; } protected virtual Track GetVirtualTrack(double emptyLength = 0) @@ -331,11 +326,6 @@ namespace osu.Game.Beatmaps protected virtual ISkin GetSkin() => new DefaultSkin(); private readonly RecyclableLazy<ISkin> skin; - ~WorkingBeatmap() - { - total_count.Value--; - } - public class RecyclableLazy<T> { private Lazy<T> lazy; diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index bb743d4ccc..1eceb56e33 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -121,7 +122,7 @@ namespace osu.Game.Collections Current.TriggerChange(); } - protected override string GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value; + protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value; protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => { @@ -139,7 +140,7 @@ namespace osu.Game.Collections public readonly Bindable<CollectionFilterMenuItem> SelectedItem = new Bindable<CollectionFilterMenuItem>(); private readonly Bindable<string> collectionName = new Bindable<string>(); - protected override string Label + protected override LocalisableString Label { get => base.Label; set { } // See updateText(). diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index fe79358223..0617996872 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -36,7 +36,19 @@ namespace osu.Game.Collections } public bool Equals(CollectionFilterMenuItem other) - => other != null && CollectionName.Value == other.CollectionName.Value; + { + if (other == null) + return false; + + // collections may have the same name, so compare first on reference equality. + // this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager. + if (Collection != null) + return Collection == other.Collection; + + // fallback to name-based comparison. + // this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below). + return CollectionName.Value == other.CollectionName.Value; + } public override int GetHashCode() => CollectionName.Value.GetHashCode(); } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index a65d9a415d..9723409c79 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -124,7 +124,7 @@ namespace osu.Game.Collections return Task.Run(async () => { using (var stream = stable.GetStream(database_name)) - await Import(stream); + await Import(stream).ConfigureAwait(false); }); } @@ -138,10 +138,10 @@ namespace osu.Game.Collections PostNotification?.Invoke(notification); - var collection = readCollections(stream, notification); - await importCollections(collection); + var collections = readCollections(stream, notification); + await importCollections(collections).ConfigureAwait(false); - notification.CompletionText = $"Imported {collection.Count} collections"; + notification.CompletionText = $"Imported {collections.Count} collections"; notification.State = ProgressNotificationState.Completed; } @@ -155,7 +155,7 @@ namespace osu.Game.Collections { foreach (var newCol in newCollections) { - var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name); + var existing = Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value); if (existing == null) Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } }); diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d0fa45bb7a..387cfbb193 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -24,126 +24,126 @@ namespace osu.Game.Configuration protected override void InitialiseDefaults() { // UI/selection defaults - Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); - Set(OsuSetting.Skin, 0, -1, int.MaxValue); + SetDefault(OsuSetting.Ruleset, 0, 0, int.MaxValue); + SetDefault(OsuSetting.Skin, 0, -1, int.MaxValue); - Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); - Set(OsuSetting.BeatmapDetailModsFilter, false); + SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); + SetDefault(OsuSetting.BeatmapDetailModsFilter, false); - Set(OsuSetting.ShowConvertedBeatmaps, true); - Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); - Set(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1); + SetDefault(OsuSetting.ShowConvertedBeatmaps, true); + SetDefault(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); + SetDefault(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1); - Set(OsuSetting.SongSelectGroupingMode, GroupMode.All); - Set(OsuSetting.SongSelectSortingMode, SortMode.Title); + SetDefault(OsuSetting.SongSelectGroupingMode, GroupMode.All); + SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title); - Set(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); + SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); - Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); + SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); // Online settings - Set(OsuSetting.Username, string.Empty); - Set(OsuSetting.Token, string.Empty); + SetDefault(OsuSetting.Username, string.Empty); + SetDefault(OsuSetting.Token, string.Empty); - Set(OsuSetting.AutomaticallyDownloadWhenSpectating, false); + SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false); - Set(OsuSetting.SavePassword, false).ValueChanged += enabled => + SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => { - if (enabled.NewValue) Set(OsuSetting.SaveUsername, true); + if (enabled.NewValue) SetValue(OsuSetting.SaveUsername, true); }; - Set(OsuSetting.SaveUsername, true).ValueChanged += enabled => + SetDefault(OsuSetting.SaveUsername, true).ValueChanged += enabled => { - if (!enabled.NewValue) Set(OsuSetting.SavePassword, false); + if (!enabled.NewValue) SetValue(OsuSetting.SavePassword, false); }; - Set(OsuSetting.ExternalLinkWarning, true); - Set(OsuSetting.PreferNoVideo, false); + SetDefault(OsuSetting.ExternalLinkWarning, true); + SetDefault(OsuSetting.PreferNoVideo, false); - Set(OsuSetting.ShowOnlineExplicitContent, false); + SetDefault(OsuSetting.ShowOnlineExplicitContent, false); // Audio - Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); + SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); - Set(OsuSetting.MenuVoice, true); - Set(OsuSetting.MenuMusic, true); + SetDefault(OsuSetting.MenuVoice, true); + SetDefault(OsuSetting.MenuMusic, true); - Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); + SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); // Input - Set(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f); - Set(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f); - Set(OsuSetting.AutoCursorSize, false); + SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f); + SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f); + SetDefault(OsuSetting.AutoCursorSize, false); - Set(OsuSetting.MouseDisableButtons, false); - Set(OsuSetting.MouseDisableWheel, false); - Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); + SetDefault(OsuSetting.MouseDisableButtons, false); + SetDefault(OsuSetting.MouseDisableWheel, false); + SetDefault(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); // Graphics - Set(OsuSetting.ShowFpsDisplay, false); + SetDefault(OsuSetting.ShowFpsDisplay, false); - Set(OsuSetting.ShowStoryboard, true); - Set(OsuSetting.BeatmapSkins, true); - Set(OsuSetting.BeatmapColours, true); - Set(OsuSetting.BeatmapHitsounds, true); + SetDefault(OsuSetting.ShowStoryboard, true); + SetDefault(OsuSetting.BeatmapSkins, true); + SetDefault(OsuSetting.BeatmapColours, true); + SetDefault(OsuSetting.BeatmapHitsounds, true); - Set(OsuSetting.CursorRotation, true); + SetDefault(OsuSetting.CursorRotation, true); - Set(OsuSetting.MenuParallax, true); + SetDefault(OsuSetting.MenuParallax, true); // Gameplay - Set(OsuSetting.DimLevel, 0.8, 0, 1, 0.01); - Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01); - Set(OsuSetting.LightenDuringBreaks, true); + SetDefault(OsuSetting.DimLevel, 0.8, 0, 1, 0.01); + SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01); + SetDefault(OsuSetting.LightenDuringBreaks, true); - Set(OsuSetting.HitLighting, true); + SetDefault(OsuSetting.HitLighting, true); - Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); - Set(OsuSetting.ShowProgressGraph, true); - Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); - Set(OsuSetting.FadePlayfieldWhenHealthLow, true); - Set(OsuSetting.KeyOverlay, false); - Set(OsuSetting.PositionalHitSounds, true); - Set(OsuSetting.AlwaysPlayFirstComboBreak, true); - Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); + SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); + SetDefault(OsuSetting.ShowProgressGraph, true); + SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); + SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); + SetDefault(OsuSetting.KeyOverlay, false); + SetDefault(OsuSetting.PositionalHitSounds, true); + SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); + SetDefault(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); - Set(OsuSetting.FloatingComments, false); + SetDefault(OsuSetting.FloatingComments, false); - Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised); + SetDefault(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised); - Set(OsuSetting.IncreaseFirstObjectVisibility, true); - Set(OsuSetting.GameplayDisableWinKey, true); + SetDefault(OsuSetting.IncreaseFirstObjectVisibility, true); + SetDefault(OsuSetting.GameplayDisableWinKey, true); // Update - Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); + SetDefault(OsuSetting.ReleaseStream, ReleaseStream.Lazer); - Set(OsuSetting.Version, string.Empty); + SetDefault(OsuSetting.Version, string.Empty); - Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); - Set(OsuSetting.ScreenshotCaptureMenuCursor, false); + SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); + SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false); - Set(OsuSetting.SongSelectRightMouseScroll, false); + SetDefault(OsuSetting.SongSelectRightMouseScroll, false); - Set(OsuSetting.Scaling, ScalingMode.Off); + SetDefault(OsuSetting.Scaling, ScalingMode.Off); - Set(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f); - Set(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f); + SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f); + SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f); - Set(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f); - Set(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f); + SetDefault(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f); + SetDefault(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f); - Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); + SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); - Set(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f); + SetDefault(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f); - Set(OsuSetting.IntroSequence, IntroSequence.Triangles); + SetDefault(OsuSetting.IntroSequence, IntroSequence.Triangles); - Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); - Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); + SetDefault(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); + SetDefault(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); - Set(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); + SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); - Set(OsuSetting.EditorWaveformOpacity, 1f); + SetDefault(OsuSetting.EditorWaveformOpacity, 1f); } public OsuConfigManager(Storage storage) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index fd401119ff..36eb6964dd 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -14,10 +14,10 @@ namespace osu.Game.Configuration { protected override void InitialiseDefaults() { - Set(Static.LoginOverlayDisplayed, false); - Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.LastHoverSoundPlaybackTime, (double?)null); - Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); + SetDefault(Static.LoginOverlayDisplayed, false); + SetDefault(Static.MutedAudioNotificationShownOnce, false); + SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); + SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); } } diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 70d67aaaa0..cfce615130 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -8,6 +8,7 @@ using System.Reflection; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Overlays.Settings; namespace osu.Game.Configuration @@ -22,9 +23,9 @@ namespace osu.Game.Configuration /// </remarks> [MeansImplicitUse] [AttributeUsage(AttributeTargets.Property)] - public class SettingSourceAttribute : Attribute + public class SettingSourceAttribute : Attribute, IComparable<SettingSourceAttribute> { - public string Label { get; } + public LocalisableString Label { get; } public string Description { get; } @@ -41,6 +42,21 @@ namespace osu.Game.Configuration { OrderPosition = orderPosition; } + + public int CompareTo(SettingSourceAttribute other) + { + if (OrderPosition == other.OrderPosition) + return 0; + + // unordered items come last (are greater than any ordered items). + if (OrderPosition == null) + return 1; + if (other.OrderPosition == null) + return -1; + + // ordered items are sorted by the order value. + return OrderPosition.Value.CompareTo(other.OrderPosition); + } } public static class SettingSourceExtensions @@ -136,14 +152,9 @@ namespace osu.Game.Configuration } } - public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) - { - var original = obj.GetSettingsSourceProperties(); - - var orderedRelative = original.Where(attr => attr.Item1.OrderPosition != null).OrderBy(attr => attr.Item1.OrderPosition); - var unordered = original.Except(orderedRelative); - - return orderedRelative.Concat(unordered); - } + public static ICollection<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) + => obj.GetSettingsSourceProperties() + .OrderBy(attr => attr.Item1) + .ToArray(); } } diff --git a/osu.Game/Configuration/StorageConfigManager.cs b/osu.Game/Configuration/StorageConfigManager.cs index 929f8f22ad..90ea42b638 100644 --- a/osu.Game/Configuration/StorageConfigManager.cs +++ b/osu.Game/Configuration/StorageConfigManager.cs @@ -19,7 +19,7 @@ namespace osu.Game.Configuration { base.InitialiseDefaults(); - Set(StorageConfig.FullPath, string.Empty); + SetDefault(StorageConfig.FullPath, string.Empty); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 03b8db2cb8..d809dbcb01 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -22,7 +22,6 @@ using osu.Game.IO.Archives; using osu.Game.IPC; using osu.Game.Overlays.Notifications; using SharpCompress.Archives.Zip; -using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Database { @@ -141,6 +140,13 @@ namespace osu.Game.Database protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks) { + if (tasks.Length == 0) + { + notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; + notification.State = ProgressNotificationState.Completed; + return Enumerable.Empty<TModel>(); + } + notification.Progress = 0; notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; @@ -156,7 +162,7 @@ namespace osu.Game.Database try { - var model = await Import(task, isLowPriorityImport, notification.CancellationToken); + var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false); lock (imported) { @@ -176,7 +182,7 @@ namespace osu.Game.Database { Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); } - })); + })).ConfigureAwait(false); if (imported.Count == 0) { @@ -219,7 +225,7 @@ namespace osu.Game.Database TModel import; using (ArchiveReader reader = task.GetReader()) - import = await Import(reader, lowPriority, cancellationToken); + import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with items from default storage. @@ -351,7 +357,7 @@ namespace osu.Game.Database item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>(); item.Hash = ComputeHash(item, archive); - await Populate(item, archive, cancellationToken); + await Populate(item, archive, cancellationToken).ConfigureAwait(false); using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { @@ -403,7 +409,7 @@ namespace osu.Game.Database flushEvents(true); return item; - }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); + }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); /// <summary> /// Exports an item to a legacy (.zip based) package. @@ -614,7 +620,7 @@ namespace osu.Game.Database } /// <summary> - /// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store. + /// Create all required <see cref="IO.FileInfo"/>s for the provided archive, adding them to the global file store. /// </summary> private List<TFileModel> createFileInfos(ArchiveReader reader, FileStore files) { @@ -692,7 +698,7 @@ namespace osu.Game.Database return Task.CompletedTask; } - return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray())); + return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false)); } /// <summary> diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index ddafd77066..84c39e3532 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -54,10 +54,5 @@ namespace osu.Game.Database Dispose(true); GC.SuppressFinalize(this); } - - ~DatabaseWriteUsage() - { - Dispose(false); - } } } diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index 50b022f9ff..da3144e8d0 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -82,7 +82,7 @@ namespace osu.Game.Database Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - var imported = await Import(notification, new ImportTask(filename)); + var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index d913e66428..a1a1279d71 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -29,7 +29,7 @@ namespace osu.Game.Database if (CheckExists(lookup, out TValue performance)) return performance; - var computed = await ComputeValueAsync(lookup, token); + var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); if (computed != null || CacheNullValues) cache[lookup] = computed; diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 568726199c..19cc211709 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -28,7 +28,7 @@ namespace osu.Game.Database public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) - => await queryUser(lookup); + => await queryUser(lookup).ConfigureAwait(false); private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>(); private Task pendingRequestTask; diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index f7914cbbca..fb7fe4947b 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -103,7 +103,7 @@ namespace osu.Game.Graphics } } - using (var image = await host.TakeScreenshotAsync()) + using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false) cursorVisibility.Value = true; @@ -116,13 +116,13 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - await image.SaveAsPngAsync(stream); + await image.SaveAsPngAsync(stream).ConfigureAwait(false); break; case ScreenshotFormat.Jpg: const int jpeg_quality = 92; - await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }); + await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false); break; default: diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 85df2d167f..fb273d7293 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osuTK; namespace osu.Game.Graphics.Sprites @@ -14,7 +15,7 @@ namespace osu.Game.Graphics.Sprites { private readonly OsuSpriteText spriteText, blurredText; - public string Text + public LocalisableString Text { get => spriteText.Text; set => blurredText.Text = spriteText.Text = value; diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index 953f3985f9..407bf6a923 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; namespace osu.Game.Graphics.UserInterface { @@ -24,11 +25,11 @@ namespace osu.Game.Graphics.UserInterface set { direction = value; - base.Direction = direction.HasFlag(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal; + base.Direction = direction.HasFlagFast(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal; foreach (var bar in Children) { - bar.Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1); + bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1); bar.Direction = direction; } } @@ -56,14 +57,14 @@ namespace osu.Game.Graphics.UserInterface if (bar.Bar != null) { bar.Bar.Length = length; - bar.Bar.Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1); + bar.Bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1); } else { Add(new Bar { RelativeSizeAxes = Axes.Both, - Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1), + Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1), Length = length, Direction = Direction, }); diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index b499b26f38..8df2c1c2fd 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface protected class TextContainer : Container, IHasText { - public string Text + public LocalisableString Text { get => NormalText.Text; set diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 9cf8f02024..a22c837080 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -21,9 +22,9 @@ namespace osu.Game.Graphics.UserInterface /// </summary> public class OsuButton : Button { - public string Text + public LocalisableString Text { - get => SpriteText?.Text; + get => SpriteText?.Text ?? default; set { if (SpriteText != null) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index cc76c12975..15fb00ccb0 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; @@ -168,7 +169,7 @@ namespace osu.Game.Graphics.UserInterface protected new class Content : FillFlowContainer, IHasText { - public string Text + public LocalisableString Text { get => Label.Text; set => Label.Text = value; @@ -215,7 +216,7 @@ namespace osu.Game.Graphics.UserInterface { protected readonly SpriteText Text; - protected override string Label + protected override LocalisableString Label { get => Text.Text; set => Text.Text = value; diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index bdc95ee048..b66a4a58ce 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -35,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface } } - public string Text + public LocalisableString Text { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs index 924c7913f3..615895074c 100644 --- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs +++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; using System.Collections.Generic; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -18,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface { private const int duration = 200; - public string Text + public LocalisableString Text { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs index 5baf794227..003a81f562 100644 --- a/osu.Game/Graphics/UserInterface/TriangleButton.cs +++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface }); } - public virtual IEnumerable<string> FilterTerms => new[] { Text }; + public virtual IEnumerable<string> FilterTerms => new[] { Text.ToString() }; public bool MatchingFilter { diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 120149d8c1..8f03c7073c 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using System; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -56,15 +57,15 @@ namespace osu.Game.Graphics.UserInterface set { base.Origin = value; - c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; - c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; + c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; + c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; - X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; + X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; Remove(c1); Remove(c2); - c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1; - c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0; + c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1; + c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0; Add(c1); Add(c2); } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 4aeda74be8..266eb11319 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 @@ -53,6 +54,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 CornerRadius = CORNER_RADIUS, }; + public override bool AcceptsFocus => true; + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + GetContainingInputManager().ChangeFocus(Component); + } + protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => { t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText); diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index f74574e60c..679ab40402 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives return null; byte[] buffer = new byte[input.Length]; - await input.ReadAsync(buffer); + await input.ReadAsync(buffer).ConfigureAwait(false); return buffer; } } diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8097f61ea4..7df5d820ee 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -58,7 +58,7 @@ namespace osu.Game.IO /// </summary> public void ResetCustomStoragePath() { - storageConfig.Set(StorageConfig.FullPath, string.Empty); + storageConfig.SetValue(StorageConfig.FullPath, string.Empty); storageConfig.Save(); ChangeTargetStorage(defaultStorage); @@ -103,7 +103,7 @@ namespace osu.Game.IO public override void Migrate(Storage newStorage) { base.Migrate(newStorage); - storageConfig.Set(StorageConfig.FullPath, newStorage.GetFullPath(".")); + storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); storageConfig.Save(); } } diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs deleted file mode 100644 index 46447b607b..0000000000 --- a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using osuTK; - -namespace osu.Game.IO.Serialization.Converters -{ - /// <summary> - /// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>. - /// </summary> - public class Vector2Converter : JsonConverter<Vector2> - { - public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) - { - var obj = JObject.Load(reader); - return new Vector2((float)obj["x"], (float)obj["y"]); - } - - public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) - { - writer.WriteStartObject(); - - writer.WritePropertyName("x"); - writer.WriteValue(value.X); - writer.WritePropertyName("y"); - writer.WriteValue(value.Y); - - writer.WriteEndObject(); - } - } -} diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index ac95d47c4b..ba188963ea 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -1,8 +1,9 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; -using osu.Game.IO.Serialization.Converters; +using osu.Framework.IO.Serialization; namespace osu.Game.IO.Serialization { @@ -28,7 +29,7 @@ namespace osu.Game.IO.Serialization Formatting = Formatting.Indented, ObjectCreationHandling = ObjectCreationHandling.Replace, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, - Converters = new JsonConverter[] { new Vector2Converter() }, + Converters = new List<JsonConverter> { new Vector2Converter() }, ContractResolver = new KeyContractResolver() }; } diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index 029908ec9d..d9d0e4c0ea 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -33,12 +33,12 @@ namespace osu.Game.IPC if (importer == null) { // we want to contact a remote osu! to handle the import. - await SendMessageAsync(new ArchiveImportMessage { Path = path }); + await SendMessageAsync(new ArchiveImportMessage { Path = path }).ConfigureAwait(false); return; } if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant())) - await importer.Import(path); + await importer.Import(path).ConfigureAwait(false); } } diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index 93ed3ca884..fba1bee0b8 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -34,8 +34,6 @@ namespace osu.Game.Input.Handlers public override bool IsActive => true; - public override int Priority => 0; - public class ReplayState<T> : IInput where T : struct { diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8ffa0221c8..944525c119 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -89,7 +89,7 @@ namespace osu.Game.Online.API thread.Start(); } - private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); + private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.SetValue(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); internal new void Schedule(Action action) => base.Schedule(action); @@ -134,7 +134,7 @@ namespace osu.Game.Online.API state.Value = APIState.Connecting; // save the username at this point, if the user requested for it to be. - config.Set(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); + config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password)) { @@ -246,7 +246,8 @@ namespace osu.Game.Online.API this.password = password; } - public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this, versionHash); + public IHubClientConnector GetHubConnector(string clientName, string endpoint) => + new HubClientConnector(clientName, endpoint, this, versionHash); public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { @@ -373,7 +374,13 @@ namespace osu.Game.Online.API public void Queue(APIRequest request) { - lock (queue) queue.Enqueue(request); + lock (queue) + { + if (state.Value == APIState.Offline) + return; + + queue.Enqueue(request); + } } private void flushQueue(bool failOldRequests = true) @@ -394,8 +401,6 @@ namespace osu.Game.Online.API public void Logout() { - flushQueue(); - password = null; authentication.Clear(); @@ -407,6 +412,7 @@ namespace osu.Game.Online.API }); state.Value = APIState.Offline; + flushQueue(); } private static User createGuestUser() => new GuestUser(); diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index a7174324d8..1a6868cfa4 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -131,8 +131,11 @@ namespace osu.Game.Online.API { } + private bool succeeded; + internal virtual void TriggerSuccess() { + succeeded = true; Success?.Invoke(); } @@ -145,10 +148,7 @@ namespace osu.Game.Online.API public void Fail(Exception e) { - if (WebRequest?.Completed == true) - return; - - if (cancelled) + if (succeeded || cancelled) return; cancelled = true; @@ -181,9 +181,13 @@ namespace osu.Game.Online.API /// <returns>Whether we are in a failed or cancelled state.</returns> private bool checkAndScheduleFailure() { - if (API == null || pendingFailure == null) return cancelled; + if (pendingFailure == null) return cancelled; + + if (API == null) + pendingFailure(); + else + API.Schedule(pendingFailure); - API.Schedule(pendingFailure); pendingFailure = null; return true; } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 943b52db88..52f2365165 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -34,8 +34,9 @@ namespace osu.Game.Online.API /// <summary> /// Provide handling logic for an arbitrary API request. + /// Should return true is a request was handled. If null or false return, the request will be failed with a <see cref="NotSupportedException"/>. /// </summary> - public Action<APIRequest> HandleRequest; + public Func<APIRequest, bool> HandleRequest; private readonly Bindable<APIState> state = new Bindable<APIState>(APIState.Online); @@ -55,7 +56,12 @@ namespace osu.Game.Online.API public virtual void Queue(APIRequest request) { - HandleRequest?.Invoke(request); + if (HandleRequest?.Invoke(request) != true) + { + // this will fail due to not receiving an APIAccess, and trigger a failure on the request. + // this is intended - any request in testing that needs non-failures should use HandleRequest. + request.Perform(this); + } } public void Perform(APIRequest request) => HandleRequest?.Invoke(request); diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs index 707c59436d..e8871bef05 100644 --- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.IO.Network; using osu.Game.Beatmaps; namespace osu.Game.Online.API.Requests @@ -15,6 +16,13 @@ namespace osu.Game.Online.API.Requests this.noVideo = noVideo; } + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Timeout = 60000; + return req; + } + protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}"; } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 5360d36f3d..f1cb02fb10 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -15,6 +15,9 @@ namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse> { + [CanBeNull] + public IReadOnlyCollection<SearchGeneral> General { get; } + public SearchCategory SearchCategory { get; } public SortCriteria SortCriteria { get; } @@ -45,6 +48,7 @@ namespace osu.Game.Online.API.Requests string query, RulesetInfo ruleset, Cursor cursor = null, + IReadOnlyCollection<SearchGeneral> general = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending, @@ -59,6 +63,7 @@ namespace osu.Game.Online.API.Requests this.ruleset = ruleset; this.cursor = cursor; + General = general; SearchCategory = searchCategory; SortCriteria = sortCriteria; SortDirection = sortDirection; @@ -75,6 +80,9 @@ namespace osu.Game.Online.API.Requests var req = base.CreateWebRequest(); req.AddParameter("q", query); + if (General != null && General.Any()) + req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToLowerInvariant()))); + if (ruleset.ID.HasValue) req.AddParameter("m", ruleset.ID.Value.ToString()); diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 036ec4d0f3..a980f4c54b 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -152,7 +152,7 @@ namespace osu.Game.Online.Chat createNewPrivateMessageRequest.Failure += exception => { - Logger.Error(exception, "Posting message failed."); + handlePostException(exception); target.ReplaceMessage(message, null); dequeueAndRun(); }; @@ -171,7 +171,7 @@ namespace osu.Game.Online.Chat req.Failure += exception => { - Logger.Error(exception, "Posting message failed."); + handlePostException(exception); target.ReplaceMessage(message, null); dequeueAndRun(); }; @@ -184,6 +184,14 @@ namespace osu.Game.Online.Chat dequeueAndRun(); } + private static void handlePostException(Exception exception) + { + if (exception is APIException apiException) + Logger.Log(apiException.Message, level: LogLevel.Important); + else + Logger.Error(exception, "Posting message failed."); + } + /// <summary> /// Posts a command locally. Commands like /help will result in a help message written in the current channel. /// </summary> diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 52042c266b..d9599481e7 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -123,13 +123,13 @@ namespace osu.Game.Online { if (attachedRequest.Progress == 1) { - State.Value = DownloadState.Importing; Progress.Value = 1; + State.Value = DownloadState.Importing; } else { - State.Value = DownloadState.Downloading; Progress.Value = attachedRequest.Progress; + State.Value = DownloadState.Downloading; attachedRequest.Failure += onRequestFailure; attachedRequest.DownloadProgressed += onRequestProgress; diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index fdb21c5000..3839762e46 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -79,7 +79,7 @@ namespace osu.Game.Online { cancelExistingConnect(); - if (!await connectionLock.WaitAsync(10000)) + if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false)) throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); try @@ -88,7 +88,7 @@ namespace osu.Game.Online { // ensure any previous connection was disposed. // this will also create a new cancellation token source. - await disconnect(false); + await disconnect(false).ConfigureAwait(false); // this token will be valid for the scope of this connection. // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. @@ -103,7 +103,7 @@ namespace osu.Game.Online // importantly, rebuild the connection each attempt to get an updated access token. CurrentConnection = buildConnection(cancellationToken); - await CurrentConnection.StartAsync(cancellationToken); + await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false); Logger.Log($"{clientName} connected!", LoggingTarget.Network); isConnected.Value = true; @@ -119,7 +119,7 @@ namespace osu.Game.Online Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network); // retry on any failure. - await Task.Delay(5000, cancellationToken); + await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } } } @@ -174,14 +174,14 @@ namespace osu.Game.Online if (takeLock) { - if (!await connectionLock.WaitAsync(10000)) + if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false)) throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck."); } try { if (CurrentConnection != null) - await CurrentConnection.DisposeAsync(); + await CurrentConnection.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 95d76f384f..4529dfd0a7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -121,6 +123,29 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } + protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource<BeatmapSetInfo>(); + var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + + req.Success += res => + { + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + return; + } + + tcs.SetResult(res.ToBeatmapSet(Rulesets)); + }; + + req.Failure += e => tcs.SetException(e); + + API.Queue(req); + + return tcs.Task; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index bfd505fb19..0f7050596f 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -17,8 +17,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Rulesets; @@ -71,7 +69,7 @@ namespace osu.Game.Online.Multiplayer /// <summary> /// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available. /// </summary> - public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); /// <summary> /// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>. @@ -85,15 +83,15 @@ namespace osu.Game.Online.Multiplayer } } + [Resolved] + protected IAPIProvider API { get; private set; } = null!; + + [Resolved] + protected RulesetStore Rulesets { get; private set; } = null!; + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - // Only exists for compatibility with old osu-server-spectator build. // Todo: Can be removed on 2021/02/26. private long defaultPlaylistItemId; @@ -133,12 +131,12 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID.Value != null); // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value); + var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false); Debug.Assert(joinedRoom != null); // Populate users. Debug.Assert(joinedRoom.Users != null); - await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); + await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await scheduleAsync(() => @@ -146,11 +144,11 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; apiRoom = room; defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0; - }, cancellationSource.Token); + }, cancellationSource.Token).ConfigureAwait(false); // Update room settings. - await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token); - }, cancellationSource.Token); + await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); } /// <summary> @@ -180,8 +178,8 @@ namespace osu.Game.Online.Multiplayer return joinOrLeaveTaskChain.Add(async () => { - await scheduledReset; - await LeaveRoomInternal(); + await scheduledReset.ConfigureAwait(false); + await LeaveRoomInternal().ConfigureAwait(false); }); } @@ -239,11 +237,11 @@ namespace osu.Game.Online.Multiplayer switch (localUser.State) { case MultiplayerUserState.Idle: - await ChangeState(MultiplayerUserState.Ready); + await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false); return; case MultiplayerUserState.Ready: - await ChangeState(MultiplayerUserState.Idle); + await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false); return; default: @@ -309,7 +307,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - await PopulateUser(user); + await PopulateUser(user).ConfigureAwait(false); Scheduler.Add(() => { @@ -488,7 +486,7 @@ namespace osu.Game.Online.Multiplayer /// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>. /// </summary> /// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param> - protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); + protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false); /// <summary> /// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>. @@ -515,30 +513,26 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); - var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => + GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() => { if (cancellationToken.IsCancellationRequested) return; - updatePlaylist(settings, res); - }; - - api.Queue(req); + updatePlaylist(settings, set.Result); + }), TaskContinuationOptions.OnlyOnRanToCompletion); }, cancellationToken); - private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) + private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet) { if (Room == null || !Room.Settings.Equals(settings)) return; Debug.Assert(apiRoom != null); - var beatmapSet = onlineSet.ToBeatmapSet(rulesets); var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); beatmap.MD5Hash = settings.BeatmapChecksum; - var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance(); var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); @@ -568,6 +562,14 @@ namespace osu.Game.Online.Multiplayer } } + /// <summary> + /// Retrieves a <see cref="BeatmapSetInfo"/> from an online source. + /// </summary> + /// <param name="beatmapId">The beatmap set ID.</param> + /// <param name="cancellationToken">A token to cancel the request.</param> + /// <returns>The <see cref="BeatmapSetInfo"/> retrieval task.</returns> + protected abstract Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default); + /// <summary> /// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>. /// </summary> diff --git a/osu.Game/Online/Rooms/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs index f652c1720d..6b559876de 100644 --- a/osu.Game/Online/Rooms/APIScoreToken.cs +++ b/osu.Game/Online/Rooms/APIScoreToken.cs @@ -8,6 +8,6 @@ namespace osu.Game.Online.Rooms public class APIScoreToken { [JsonProperty("id")] - public int ID { get; set; } + public long ID { get; set; } } } diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs index d6f4c45a75..8278162353 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs @@ -45,6 +45,9 @@ namespace osu.Game.Online.Rooms Progress.BindValueChanged(_ => { + if (State.Value != DownloadState.Downloading) + return; + // incoming progress changes are going to be at a very high rate. // we don't want to flood the network with this, so rate limit how often we send progress updates. if (progressUpdate?.Completed != false) diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs new file mode 100644 index 0000000000..ae5ac5e26c --- /dev/null +++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; + +namespace osu.Game.Online.Solo +{ + public class CreateSoloScoreRequest : APIRequest<APIScoreToken> + { + private readonly int beatmapId; + private readonly string versionHash; + + public CreateSoloScoreRequest(int beatmapId, string versionHash) + { + this.beatmapId = beatmapId; + this.versionHash = versionHash; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + req.AddParameter("version_hash", versionHash); + return req; + } + + protected override string Target => $@"solo/{beatmapId}/scores"; + } +} diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs new file mode 100644 index 0000000000..98ba4fa052 --- /dev/null +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Online.Solo +{ + public class SubmitSoloScoreRequest : APIRequest<MultiplayerScore> + { + private readonly long scoreId; + + private readonly int beatmapId; + + private readonly ScoreInfo scoreInfo; + + public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo) + { + this.beatmapId = beatmapId; + this.scoreId = scoreId; + this.scoreInfo = scoreInfo; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Put; + + req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + })); + + return req; + } + + protected override string Target => $@"solo/{beatmapId}/scores/{scoreId}"; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 771bcd2310..dd1fa32ad9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -361,14 +361,6 @@ namespace osu.Game PerformFromScreen(screen => { - // we might already be at song select, so a check is required before performing the load to solo. - if (screen is MainMenu) - menuScreen.LoadToSolo(); - - // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) - return; - // Find beatmaps that match our predicate. var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); @@ -381,9 +373,16 @@ namespace osu.Game ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) ?? beatmaps.First(); - Ruleset.Value = selection.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); - }, validScreens: new[] { typeof(SongSelect) }); + if (screen is IHandlePresentBeatmap presentableScreen) + { + presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); + } + else + { + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); + } + }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) }); } /// <summary> @@ -441,7 +440,7 @@ namespace osu.Game public override Task Import(params ImportTask[] imports) { // encapsulate task as we don't want to begin the import process until in a ready state. - var importTask = new Task(async () => await base.Import(imports)); + var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false)); waitForReady(() => this, _ => importTask.Start()); @@ -532,6 +531,13 @@ namespace osu.Game SentryLogger.Dispose(); } + protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() + => new Dictionary<FrameworkSetting, object> + { + // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance) + { FrameworkSetting.WindowMode, WindowMode.Fullscreen } + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -759,9 +765,15 @@ namespace osu.Game { otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); - // show above others if not visible at all, else leave at current depth. - if (!overlay.IsPresent) + // Partially visible so leave it at the current depth. + if (overlay.IsPresent) + return; + + // Show above all other overlays. + if (overlay.IsLoaded) overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime); + else + overlay.Depth = (float)-Clock.CurrentTime; } private void forwardLoggedErrorsToNotifications() @@ -832,7 +844,7 @@ namespace osu.Game asyncLoadStream = Task.Run(async () => { if (previousLoadStream != null) - await previousLoadStream; + await previousLoadStream.ConfigureAwait(false); try { @@ -846,7 +858,7 @@ namespace osu.Game // The delegate won't complete if OsuGame has been disposed in the meantime while (!IsDisposed && !del.Completed) - await Task.Delay(10); + await Task.Delay(10).ConfigureAwait(false); // Either we're disposed or the load process has started successfully if (IsDisposed) @@ -854,7 +866,7 @@ namespace osu.Game Debug.Assert(task != null); - await task; + await task.ConfigureAwait(false); Logger.Log($"Loaded {component}!", level: LogLevel.Debug); } @@ -881,22 +893,13 @@ namespace osu.Game switch (action) { case GlobalAction.ResetInputSettings: - var sensitivity = frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity); - - sensitivity.Disabled = false; - sensitivity.Value = 1; - sensitivity.Disabled = true; - - frameworkConfig.Set(FrameworkSetting.IgnoredInputHandlers, string.Empty); + Host.ResetInputHandlers(); frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault(); return true; - case GlobalAction.ToggleToolbar: - Toolbar.ToggleVisibility(); - return true; - case GlobalAction.ToggleGameplayMouseButtons: - LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get<bool>(OsuSetting.MouseDisableButtons)); + var mouseDisableButtons = LocalConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons); + mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; case GlobalAction.RandomSkin: diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3d24f245f9..e1c7b67a8c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -434,7 +434,7 @@ namespace osu.Game foreach (var importer in fileImporters) { if (importer.HandledExtensions.Contains(extension)) - await importer.Import(paths); + await importer.Import(paths).ConfigureAwait(false); } } @@ -445,7 +445,7 @@ namespace osu.Game { var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key)); return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask; - })); + })).ConfigureAwait(false); } public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index bcc5a91677..1935a250b7 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -134,6 +134,7 @@ namespace osu.Game.Overlays.BeatmapListing queueUpdateSearch(true); }); + searchControl.General.CollectionChanged += (_, __) => queueUpdateSearch(); searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch()); searchControl.Category.BindValueChanged(_ => queueUpdateSearch()); searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); @@ -187,6 +188,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Query.Value, searchControl.Ruleset.Value, lastResponse?.Cursor, + searchControl.General, searchControl.Category.Value, sortControl.Current.Value, sortControl.SortDirection.Value, diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index b138a5ac52..1576431d40 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -29,6 +29,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable<string> Query => textBox.Current; + public BindableList<SearchGeneral> General => generalFilter.Current; + public Bindable<RulesetInfo> Ruleset => modeFilter.Current; public Bindable<SearchCategory> Category => categoryFilter.Current; @@ -61,6 +63,7 @@ namespace osu.Game.Overlays.BeatmapListing } private readonly BeatmapSearchTextBox textBox; + private readonly BeatmapSearchMultipleSelectionFilterRow<SearchGeneral> generalFilter; private readonly BeatmapSearchRulesetFilterRow modeFilter; private readonly BeatmapSearchFilterRow<SearchCategory> categoryFilter; private readonly BeatmapSearchFilterRow<SearchGenre> genreFilter; @@ -123,6 +126,7 @@ namespace osu.Game.Overlays.BeatmapListing Padding = new MarginPadding { Horizontal = 10 }, Children = new Drawable[] { + generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(@"General"), modeFilter = new BeatmapSearchRulesetFilterRow(), categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(@"Categories"), genreFilter = new BeatmapSearchFilterRow<SearchGenre>(@"Genre"), diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index c1d366bb82..4d5c387c4a 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -84,14 +84,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), + Text = new RomanisableString(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) }, } }, new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist)), + Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) }, }, diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 76a30d1c11..00ffd168c1 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -107,14 +107,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), + Text = new RomanisableString(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) }, } }, new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist)), + Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) }, } diff --git a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs new file mode 100644 index 0000000000..175942c626 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; + +namespace osu.Game.Overlays.BeatmapListing +{ + public enum SearchGeneral + { + [Description("Recommended difficulty")] + Recommended, + + [Description("Include converted beatmaps")] + Converts, + + [Description("Subscribed mappers")] + Follows + } +} diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index a2464bef09..cf74c0d4d3 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -96,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet public string TooltipText { get; } - public string Value + public LocalisableString Value { get => value.Text; set => this.value.Text = value; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 93744dd6a3..5cb834b510 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -204,7 +203,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores this.text = text; } - public LocalisedString Text + public string Text { set => text.Text = value; } diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 537dd00727..e7d68853ad 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -21,6 +21,8 @@ namespace osu.Game.Overlays { public class ChangelogOverlay : OnlineOverlay<ChangelogHeader> { + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>(); private Sample sampleBack; @@ -126,8 +128,11 @@ namespace osu.Game.Overlays private Task initialFetchTask; - private void performAfterFetch(Action action) => fetchListing()?.ContinueWith(_ => - Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); + private void performAfterFetch(Action action) => Schedule(() => + { + fetchListing()?.ContinueWith(_ => + Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); + }); private Task fetchListing() { @@ -160,10 +165,10 @@ namespace osu.Game.Overlays tcs.SetException(e); }; - await API.PerformAsync(req); + await API.PerformAsync(req).ConfigureAwait(false); - await tcs.Task; - }); + return tcs.Task; + }).Unwrap(); } private CancellationTokenSource loadContentCancellation; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 86ce724390..5f9c00b36a 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -275,7 +275,8 @@ namespace osu.Game.Overlays.Chat { if (!UserScrolling) { - ScrollToEnd(); + if (Current < ScrollableExtent) + ScrollToEnd(); lastExtent = ScrollableExtent; } }); diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs index eac48ca5cb..537ac975ac 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs @@ -4,19 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; -using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; +using osuTK; namespace osu.Game.Overlays.Chat.Selection { public class ChannelSection : Container, IHasFilterableChildren { - private readonly OsuSpriteText header; - public readonly FillFlowContainer<ChannelListItem> ChannelFlow; public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children; @@ -29,12 +27,6 @@ namespace osu.Game.Overlays.Chat.Selection public bool FilteringActive { get; set; } - public string Header - { - get => header.Text; - set => header.Text = value.ToUpperInvariant(); - } - public IEnumerable<Channel> Channels { set => ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c)); @@ -47,9 +39,10 @@ namespace osu.Game.Overlays.Chat.Selection Children = new Drawable[] { - header = new OsuSpriteText + new OsuSpriteText { Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), + Text = "All Channels".ToUpperInvariant() }, ChannelFlow = new FillFlowContainer<ChannelListItem> { diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index be9ecc6746..231d7ca63c 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -131,11 +131,7 @@ namespace osu.Game.Overlays.Chat.Selection { sectionsFlow.ChildrenEnumerable = new[] { - new ChannelSection - { - Header = "All Channels", - Channels = channels, - }, + new ChannelSection { Channels = channels, }, }; foreach (ChannelSection s in sectionsFlow.Children) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 57bf2af4d2..2f7f16dd6f 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Comments.Buttons { public abstract class CommentRepliesButton : CompositeDrawable { - protected string Text + protected LocalisableString Text { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs index 56588ef0a8..8c40d79f7a 100644 --- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs +++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments { new SpriteIcon { - Icon = FontAwesome.Solid.Trash, + Icon = FontAwesome.Regular.TrashAlt, Size = new Vector2(14), }, countText = new OsuSpriteText diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index e6fe6ac749..0922ce5ecc 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -244,7 +244,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return unsorted.OrderByDescending(u => u.LastVisit).ToList(); case UserSortCriteria.Rank: - return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList(); + return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList(); case UserSortCriteria.Username: return unsorted.OrderBy(u => u.Username).ToList(); diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs new file mode 100644 index 0000000000..a87c06ffdf --- /dev/null +++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; + +namespace osu.Game.Overlays.Dialog +{ + /// <summary> + /// A dialog which confirms a user action. + /// </summary> + public class ConfirmDialog : PopupDialog + { + /// <summary> + /// Construct a new confirmation dialog. + /// </summary> + /// <param name="message">The description of the action to be displayed to the user.</param> + /// <param name="onConfirm">An action to perform on confirmation.</param> + /// <param name="onCancel">An optional action to perform on cancel.</param> + public ConfirmDialog(string message, Action onConfirm, Action onCancel = null) + { + HeaderText = message; + BodyText = "Last chance to turn back"; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Yes", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"Cancel", + Action = onCancel + }, + }; + } + } +} diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index b808d49fa2..300fce962a 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer cancelAndClearButtons; private FillFlowContainer<KeyButton> buttons; - public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); + public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString()); public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings) { diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index c3e56abd05..aa8a5efd39 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -23,13 +23,15 @@ namespace osu.Game.Overlays.Mods public FillFlowContainer<ModButtonEmpty> ButtonsContainer { get; } + protected IReadOnlyList<ModButton> Buttons { get; private set; } = Array.Empty<ModButton>(); + public Action<Mod> Action; public Key[] ToggleKeys; public readonly ModType ModType; - public IEnumerable<Mod> SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); + public IEnumerable<Mod> SelectedMods => Buttons.Select(b => b.SelectedMod).Where(m => m != null); private CancellationTokenSource modsLoadCts; @@ -77,7 +79,7 @@ namespace osu.Game.Overlays.Mods ButtonsContainer.ChildrenEnumerable = c; }, (modsLoadCts = new CancellationTokenSource()).Token); - buttons = modContainers.OfType<ModButton>().ToArray(); + Buttons = modContainers.OfType<ModButton>().ToArray(); header.FadeIn(200); this.FadeIn(200); @@ -88,8 +90,6 @@ namespace osu.Game.Overlays.Mods { } - private ModButton[] buttons = Array.Empty<ModButton>(); - protected override bool OnKeyDown(KeyDownEvent e) { if (e.ControlPressed) return false; @@ -97,8 +97,8 @@ namespace osu.Game.Overlays.Mods if (ToggleKeys != null) { var index = Array.IndexOf(ToggleKeys, e.Key); - if (index > -1 && index < buttons.Length) - buttons[index].SelectNext(e.ShiftPressed ? -1 : 1); + if (index > -1 && index < Buttons.Count) + Buttons[index].SelectNext(e.ShiftPressed ? -1 : 1); } return base.OnKeyDown(e); @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in buttons.Where(b => !b.Selected)) + foreach (var button in Buttons.Where(b => !b.Selected)) pendingSelectionOperations.Enqueue(() => button.SelectAt(0)); } @@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Mods public void DeselectAll() { pendingSelectionOperations.Clear(); - DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + DeselectTypes(Buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); } /// <summary> @@ -161,7 +161,7 @@ namespace osu.Game.Overlays.Mods /// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param> public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false) { - foreach (var button in buttons) + foreach (var button in Buttons) { if (button.SelectedMod == null) continue; @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Mods /// <param name="newSelectedMods">The new list of selected mods to select.</param> public void UpdateSelectedButtons(IReadOnlyList<Mod> newSelectedMods) { - foreach (var button in buttons) + foreach (var button in Buttons) updateButtonSelection(button, newSelectedMods); } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index eef91deb4c..26b8632d7f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -456,6 +456,7 @@ namespace osu.Game.Overlays.Mods } updateSelectedButtons(); + OnAvailableModsChanged(); } /// <summary> @@ -533,6 +534,13 @@ namespace osu.Game.Overlays.Mods private void playSelectedSound() => sampleOn?.Play(); private void playDeselectedSound() => sampleOff?.Play(); + /// <summary> + /// Invoked after <see cref="availableMods"/> has changed. + /// </summary> + protected virtual void OnAvailableModsChanged() + { + } + /// <summary> /// Invoked when a new <see cref="Mod"/> has been selected. /// </summary> diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 96dff39fae..571b14428e 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -48,8 +48,8 @@ namespace osu.Game.Overlays.Music artistColour = colours.Gray9; HandleColour = colours.Gray5; - title = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title))); - artist = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist))); + title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.TitleUnicode, Model.Metadata.Title)); + artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.ArtistUnicode, Model.Metadata.Artist)); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 38ba712254..2316199049 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -8,10 +8,11 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Notifications { @@ -37,7 +38,7 @@ namespace osu.Game.Overlays.Notifications public NotificationSection(string title, string clearButtonText) { - this.clearButtonText = clearButtonText; + this.clearButtonText = clearButtonText.ToUpperInvariant(); titleText = title; } @@ -121,7 +122,20 @@ namespace osu.Game.Overlays.Notifications { base.Update(); - countDrawable.Text = notifications.Children.Count(c => c.Alpha > 0.99f).ToString(); + countDrawable.Text = getVisibleCount().ToString(); + } + + private int getVisibleCount() + { + int count = 0; + + foreach (var c in notifications) + { + if (c.Alpha > 0.99f) + count++; + } + + return count; } private class ClearAllButton : OsuClickableContainer @@ -138,10 +152,10 @@ namespace osu.Game.Overlays.Notifications }; } - public string Text + public LocalisableString Text { get => text.Text; - set => text.Text = value.ToUpperInvariant(); + set => text.Text = value; } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 2866d2ad6d..81bf71cdec 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -293,8 +293,8 @@ namespace osu.Game.Overlays else { BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); - artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); + title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); + artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); } }); diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index b2212336ef..0ebabd424f 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.Comments; using JetBrains.Annotations; using System; using osu.Framework.Extensions; +using osu.Framework.Localisation; namespace osu.Game.Overlays { @@ -30,7 +31,7 @@ namespace osu.Game.Overlays set => current.Current = value; } - public string Title + public LocalisableString Title { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 2925107766..fe61e532e1 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -113,7 +114,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + + addSpacer(topLinkContainer); + + topLinkContainer.AddText("Posted "); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; @@ -138,7 +144,6 @@ namespace osu.Game.Overlays.Profile.Header if (!string.IsNullOrEmpty(user.Twitter)) anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}"); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord); - anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat"); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website); // If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 5b7c5efbe2..20e40569e8 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -13,6 +12,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections.Historical { @@ -129,14 +129,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { new OsuSpriteText { - Text = new LocalisedString(( + Text = new RomanisableString( $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ")), + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] "), Font = OsuFont.GetFont(weight: FontWeight.Bold) }, new OsuSpriteText { - Text = "by " + new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), + Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Regular) }, }; diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs index d4d0976724..cdb24b784c 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs @@ -23,51 +23,24 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu { this.user.BindTo(user); CountSection total; - CountSection avaliable; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 3; - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Children = new[] - { - total = new CountTotal(), - avaliable = new CountAvailable() - } - } - }; - this.user.ValueChanged += u => - { - total.Count = u.NewValue?.Kudosu.Total ?? 0; - avaliable.Count = u.NewValue?.Kudosu.Available ?? 0; - }; + Child = total = new CountTotal(); + + this.user.ValueChanged += u => total.Count = u.NewValue?.Kudosu.Total ?? 0; } protected override bool OnClick(ClickEvent e) => true; - private class CountAvailable : CountSection - { - public CountAvailable() - : base("Kudosu Avaliable") - { - DescriptionText.Text = "Kudosu can be traded for kudosu stars, which will help your beatmap get more attention. This is the number of kudosu you haven't traded in yet."; - } - } - private class CountTotal : CountSection { public CountTotal() : base("Total Kudosu Earned") { DescriptionText.AddText("Based on how much of a contribution the user has made to beatmap moderation. See "); - DescriptionText.AddLink("this link", "https://osu.ppy.sh/wiki/Kudosu"); + DescriptionText.AddLink("this page", "https://osu.ppy.sh/wiki/Kudosu"); DescriptionText.AddText(" for more information."); } } @@ -80,13 +53,12 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu public new int Count { - set => valueText.Text = value.ToString(); + set => valueText.Text = value.ToString("N0"); } public CountSection(string header) { RelativeSizeAxes = Axes.X; - Width = 0.5f; AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Top = 10, Bottom = 20 }; Child = new FillFlowContainer @@ -131,7 +103,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu private void load(OverlayColourProvider colourProvider) { lineBackground.Colour = colourProvider.Highlight1; - DescriptionText.Colour = colourProvider.Foreground1; } } } diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 2c20dcc0ef..713303285a 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -256,16 +256,16 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = new LocalisedString(( + Text = new RomanisableString( $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ")), + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} "), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) }, new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "by " + new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), + Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), Font = OsuFont.GetFont(size: 12, italics: true) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index bed74542c9..b31e7dc45b 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -76,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private class AudioDeviceDropdownControl : DropdownControl { - protected override string GenerateItemText(string item) + protected override LocalisableString GenerateItemText(string item) => string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item); } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 7acbf038d8..4d5c2e06eb 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -234,7 +235,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private class ResolutionDropdownControl : DropdownControl { - protected override string GenerateItemText(Size item) + protected override LocalisableString GenerateItemText(Size item) { if (item == new Size(9999, 9999)) return "Default"; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs similarity index 70% rename from osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs rename to osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs index db6f24a954..79c73863cf 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs @@ -5,17 +5,17 @@ using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings.Sections.Input { - public class KeyboardSettings : SettingsSubsection + public class BindingSettings : SettingsSubsection { - protected override string Header => "Keyboard"; + protected override string Header => "Shortcut and gameplay bindings"; - public KeyboardSettings(KeyBindingPanel keyConfig) + public BindingSettings(KeyBindingPanel keyConfig) { Children = new Drawable[] { new SettingsButton { - Text = "Key configuration", + Text = "Configure", TooltipText = "change global shortcut keys and gameplay bindings", Action = keyConfig.ToggleVisibility }, diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 455e13711d..fb908a7669 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -1,11 +1,11 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Input.Handlers.Mouse; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -14,41 +14,44 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public class MouseSettings : SettingsSubsection { + private readonly MouseHandler mouseHandler; + protected override string Header => "Mouse"; - private readonly BindableBool rawInputToggle = new BindableBool(); - private Bindable<double> sensitivityBindable = new BindableDouble(); - private Bindable<string> ignoredInputHandlers; + private Bindable<double> handlerSensitivity; + + private Bindable<double> localSensitivity; private Bindable<WindowMode> windowMode; private SettingsEnumDropdown<OsuConfineMouseMode> confineMouseModeSetting; + private Bindable<bool> relativeMode; + + public MouseSettings(MouseHandler mouseHandler) + { + this.mouseHandler = mouseHandler; + } [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) { - var configSensitivity = config.GetBindable<double>(FrameworkSetting.CursorSensitivity); - // use local bindable to avoid changing enabled state of game host's bindable. - sensitivityBindable = configSensitivity.GetUnboundCopy(); - configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue); - sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue); + handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy(); + localSensitivity = handlerSensitivity.GetUnboundCopy(); + + relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy(); + windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode); Children = new Drawable[] { new SettingsCheckbox { - LabelText = "Raw input", - Current = rawInputToggle + LabelText = "High precision mouse", + Current = relativeMode }, new SensitivitySetting { LabelText = "Cursor sensitivity", - Current = sensitivityBindable - }, - new SettingsCheckbox - { - LabelText = "Map absolute input to window", - Current = config.GetBindable<bool>(FrameworkSetting.MapAbsoluteInputToWindow) + Current = localSensitivity }, confineMouseModeSetting = new SettingsEnumDropdown<OsuConfineMouseMode> { @@ -66,36 +69,40 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons) }, }; + } - windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode); - windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1, true); + protected override void LoadComplete() + { + base.LoadComplete(); - if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) + relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true); + + handlerSensitivity.BindValueChanged(val => { - rawInputToggle.Disabled = true; - sensitivityBindable.Disabled = true; - } - else + var disabled = localSensitivity.Disabled; + + localSensitivity.Disabled = false; + localSensitivity.Value = val.NewValue; + localSensitivity.Disabled = disabled; + }, true); + + localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue); + + windowMode.BindValueChanged(mode => { - rawInputToggle.ValueChanged += enabled => + var isFullscreen = mode.NewValue == WindowMode.Fullscreen; + + if (isFullscreen) { - // this is temporary until we support per-handler settings. - const string raw_mouse_handler = @"OsuTKRawMouseHandler"; - const string standard_mouse_handlers = @"OsuTKMouseHandler MouseHandler"; - - ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler; - }; - - ignoredInputHandlers = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers); - ignoredInputHandlers.ValueChanged += handler => + confineMouseModeSetting.Current.Disabled = true; + confineMouseModeSetting.TooltipText = "Not applicable in full screen mode"; + } + else { - bool raw = !handler.NewValue.Contains("Raw"); - rawInputToggle.Value = raw; - sensitivityBindable.Disabled = !raw; - }; - - ignoredInputHandlers.TriggerChange(); - } + confineMouseModeSetting.Current.Disabled = false; + confineMouseModeSetting.TooltipText = string.Empty; + } + }, true); } private class SensitivitySetting : SettingsSlider<double, SensitivitySlider> @@ -109,7 +116,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private class SensitivitySlider : OsuSliderBar<double> { - public override string TooltipText => Current.Disabled ? "enable raw input to adjust sensitivity" : $"{base.TooltipText}x"; + public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x"; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs new file mode 100644 index 0000000000..ecb8acce54 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -0,0 +1,185 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Handlers.Tablet; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public class TabletAreaSelection : CompositeDrawable + { + private readonly ITabletHandler handler; + + private Container tabletContainer; + private Container usableAreaContainer; + + private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>(); + private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>(); + + private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>(); + + private OsuSpriteText tabletName; + + private Box usableFill; + private OsuSpriteText usableAreaText; + + public TabletAreaSelection(ITabletHandler handler) + { + this.handler = handler; + + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = tabletContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + CornerRadius = 5, + BorderThickness = 2, + BorderColour = colour.Gray3, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour.Gray1, + }, + usableAreaContainer = new Container + { + Origin = Anchor.Centre, + Children = new Drawable[] + { + usableFill = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.6f, + }, + new Box + { + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 5, + }, + new Box + { + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 5, + }, + usableAreaText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Default.With(size: 12), + Y = 10 + } + } + }, + tabletName = new OsuSpriteText + { + Padding = new MarginPadding(3), + Font = OsuFont.Default.With(size: 8) + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + areaOffset.BindTo(handler.AreaOffset); + areaOffset.BindValueChanged(val => + { + usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint) + .OnComplete(_ => checkBounds()); // required as we are using SSDQ. + }, true); + + areaSize.BindTo(handler.AreaSize); + areaSize.BindValueChanged(val => + { + usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint) + .OnComplete(_ => checkBounds()); // required as we are using SSDQ. + + int x = (int)val.NewValue.X; + int y = (int)val.NewValue.Y; + int commonDivider = greatestCommonDivider(x, y); + + usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}"; + }, true); + + tablet.BindTo(handler.Tablet); + tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails)); + + updateTabletDetails(); + // initial animation should be instant. + FinishTransforms(true); + } + + private void updateTabletDetails() + { + tabletContainer.Size = tablet.Value?.Size ?? Vector2.Zero; + tabletName.Text = tablet.Value?.Name ?? string.Empty; + checkBounds(); + } + + private static int greatestCommonDivider(int a, int b) + { + while (b != 0) + { + int remainder = a % b; + a = b; + b = remainder; + } + + return a; + } + + [Resolved] + private OsuColour colour { get; set; } + + private void checkBounds() + { + if (tablet.Value == null) + return; + + var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad; + + bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft + new Vector2(1)) && + tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1)); + + usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100); + } + + protected override void Update() + { + base.Update(); + + if (!(tablet.Value?.Size is Vector2 size)) + return; + + float fitX = size.X / (DrawWidth - Padding.Left - Padding.Right); + float fitY = size.Y / DrawHeight; + + float adjust = MathF.Max(fitX, fitY); + + tabletContainer.Scale = new Vector2(1 / adjust); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs new file mode 100644 index 0000000000..bd0f7ddc4c --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -0,0 +1,285 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Handlers.Tablet; +using osu.Framework.Platform; +using osu.Framework.Threading; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public class TabletSettings : SettingsSubsection + { + private readonly ITabletHandler tabletHandler; + + private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>(); + private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>(); + private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>(); + + private readonly BindableNumber<float> offsetX = new BindableNumber<float> { MinValue = 0 }; + private readonly BindableNumber<float> offsetY = new BindableNumber<float> { MinValue = 0 }; + + private readonly BindableNumber<float> sizeX = new BindableNumber<float> { MinValue = 10 }; + private readonly BindableNumber<float> sizeY = new BindableNumber<float> { MinValue = 10 }; + + [Resolved] + private GameHost host { get; set; } + + /// <summary> + /// Based on ultrawide monitor configurations. + /// </summary> + private const float largest_feasible_aspect_ratio = 21f / 9; + + private readonly BindableNumber<float> aspectRatio = new BindableFloat(1) + { + MinValue = 1 / largest_feasible_aspect_ratio, + MaxValue = largest_feasible_aspect_ratio, + Precision = 0.01f, + }; + + private readonly BindableBool aspectLock = new BindableBool(); + + private ScheduledDelegate aspectRatioApplication; + + private FillFlowContainer mainSettings; + + private OsuSpriteText noTabletMessage; + + protected override string Header => "Tablet"; + + public TabletSettings(ITabletHandler tabletHandler) + { + this.tabletHandler = tabletHandler; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "Enabled", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Current = tabletHandler.Enabled + }, + noTabletMessage = new OsuSpriteText + { + Text = "No tablet detected!", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS } + }, + mainSettings = new FillFlowContainer + { + Alpha = 0, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 8), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TabletAreaSelection(tabletHandler) + { + RelativeSizeAxes = Axes.X, + Height = 300, + }, + new DangerousSettingsButton + { + Text = "Reset to full area", + Action = () => + { + aspectLock.Value = false; + + areaOffset.SetDefault(); + areaSize.SetDefault(); + }, + }, + new SettingsButton + { + Text = "Conform to current game aspect ratio", + Action = () => + { + forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height); + } + }, + new SettingsSlider<float> + { + TransferValueOnCommit = true, + LabelText = "Aspect Ratio", + Current = aspectRatio + }, + new SettingsSlider<float> + { + TransferValueOnCommit = true, + LabelText = "X Offset", + Current = offsetX + }, + new SettingsSlider<float> + { + TransferValueOnCommit = true, + LabelText = "Y Offset", + Current = offsetY + }, + new SettingsCheckbox + { + LabelText = "Lock aspect ratio", + Current = aspectLock + }, + new SettingsSlider<float> + { + TransferValueOnCommit = true, + LabelText = "Width", + Current = sizeX + }, + new SettingsSlider<float> + { + TransferValueOnCommit = true, + LabelText = "Height", + Current = sizeY + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + areaOffset.BindTo(tabletHandler.AreaOffset); + areaOffset.BindValueChanged(val => + { + offsetX.Value = val.NewValue.X; + offsetY.Value = val.NewValue.Y; + }, true); + + offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y)); + offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue)); + + areaSize.BindTo(tabletHandler.AreaSize); + areaSize.BindValueChanged(val => + { + sizeX.Value = val.NewValue.X; + sizeY.Value = val.NewValue.Y; + }, true); + + sizeX.BindValueChanged(val => + { + areaSize.Value = new Vector2(val.NewValue, areaSize.Value.Y); + + aspectRatioApplication?.Cancel(); + aspectRatioApplication = Schedule(() => applyAspectRatio(sizeX)); + }); + + sizeY.BindValueChanged(val => + { + areaSize.Value = new Vector2(areaSize.Value.X, val.NewValue); + + aspectRatioApplication?.Cancel(); + aspectRatioApplication = Schedule(() => applyAspectRatio(sizeY)); + }); + + updateAspectRatio(); + aspectRatio.BindValueChanged(aspect => + { + aspectRatioApplication?.Cancel(); + aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue)); + }); + + tablet.BindTo(tabletHandler.Tablet); + tablet.BindValueChanged(val => + { + Scheduler.AddOnce(toggleVisibility); + + var tab = val.NewValue; + + bool tabletFound = tab != null; + if (!tabletFound) + return; + + offsetX.MaxValue = tab.Size.X; + offsetX.Default = tab.Size.X / 2; + sizeX.Default = sizeX.MaxValue = tab.Size.X; + + offsetY.MaxValue = tab.Size.Y; + offsetY.Default = tab.Size.Y / 2; + sizeY.Default = sizeY.MaxValue = tab.Size.Y; + + areaSize.Default = new Vector2(sizeX.Default, sizeY.Default); + }, true); + } + + private void toggleVisibility() + { + bool tabletFound = tablet.Value != null; + + if (!tabletFound) + { + mainSettings.Hide(); + noTabletMessage.Show(); + return; + } + + mainSettings.Show(); + noTabletMessage.Hide(); + } + + private void applyAspectRatio(BindableNumber<float> sizeChanged) + { + try + { + if (!aspectLock.Value) + { + float proposedAspectRatio = currentAspectRatio; + + if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue) + { + // aspect ratio was in a valid range. + updateAspectRatio(); + return; + } + } + + // if lock is applied (or the specified values were out of range) aim to adjust the axis the user was not adjusting to conform. + if (sizeChanged == sizeX) + sizeY.Value = (int)(areaSize.Value.X / aspectRatio.Value); + else + sizeX.Value = (int)(areaSize.Value.Y * aspectRatio.Value); + } + finally + { + // cancel any event which may have fired while updating variables as a result of aspect ratio limitations. + // this avoids a potential feedback loop. + aspectRatioApplication?.Cancel(); + } + } + + private void forceAspectRatio(float aspectRatio) + { + aspectLock.Value = false; + + int proposedHeight = (int)(sizeX.Value / aspectRatio); + + if (proposedHeight < sizeY.MaxValue) + sizeY.Value = proposedHeight; + else + sizeX.Value = (int)(sizeY.Value * aspectRatio); + + updateAspectRatio(); + + aspectRatioApplication?.Cancel(); + aspectLock.Value = true; + } + + private void updateAspectRatio() => aspectRatio.Value = currentAspectRatio; + + private float currentAspectRatio => sizeX.Value / sizeY.Value; + } +} diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index b43453f53d..6e99891794 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -1,28 +1,106 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Handlers; +using osu.Framework.Input.Handlers.Joystick; +using osu.Framework.Input.Handlers.Midi; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Input.Handlers.Tablet; +using osu.Framework.Platform; using osu.Game.Overlays.Settings.Sections.Input; namespace osu.Game.Overlays.Settings.Sections { public class InputSection : SettingsSection { + private readonly KeyBindingPanel keyConfig; + public override string Header => "Input"; + [Resolved] + private GameHost host { get; set; } + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.Keyboard }; public InputSection(KeyBindingPanel keyConfig) + { + this.keyConfig = keyConfig; + } + + [BackgroundDependencyLoader] + private void load() { Children = new Drawable[] { - new MouseSettings(), - new KeyboardSettings(keyConfig), + new BindingSettings(keyConfig), }; + + foreach (var handler in host.AvailableInputHandlers) + { + var handlerSection = createSectionFor(handler); + + if (handlerSection != null) + Add(handlerSection); + } + } + + private SettingsSubsection createSectionFor(InputHandler handler) + { + SettingsSubsection section; + + switch (handler) + { + // ReSharper disable once SuspiciousTypeConversion.Global (net standard fuckery) + case ITabletHandler th: + section = new TabletSettings(th); + break; + + case MouseHandler mh: + section = new MouseSettings(mh); + break; + + // whitelist the handlers which should be displayed to avoid any weird cases of users touching settings they shouldn't. + case JoystickHandler _: + case MidiHandler _: + section = new HandlerSection(handler); + break; + + default: + return null; + } + + return section; + } + + private class HandlerSection : SettingsSubsection + { + private readonly InputHandler handler; + + public HandlerSection(InputHandler handler) + { + this.handler = handler; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "Enabled", + Current = handler.Enabled + }, + }; + } + + protected override string Header => handler.Description; } } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 7c8309fd56..316837d27d 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -178,7 +179,7 @@ namespace osu.Game.Overlays.Settings.Sections private class SkinDropdownControl : DropdownControl { - protected override string GenerateItemText(SkinInfo item) => item.ToString(); + protected override LocalisableString GenerateItemText(SkinInfo item) => item.ToString(); } } diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index 437b2e45b3..8b7ac80a5b 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -2,20 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { public class SettingsCheckbox : SettingsItem<bool> { - private string labelText; + private LocalisableString labelText; protected override Drawable CreateControl() => new OsuCheckbox(); - public override string LabelText + public override LocalisableString LabelText { get => labelText; - set => ((OsuCheckbox)Control).LabelText = labelText = value; + // checkbox doesn't properly support localisation yet. + set => ((OsuCheckbox)Control).LabelText = (labelText = value).ToString(); } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index af225889da..85765bf991 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Settings public string TooltipText { get; set; } - public virtual string LabelText + public virtual LocalisableString LabelText { get => labelText?.Text ?? string.Empty; set @@ -69,7 +70,7 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } - public virtual IEnumerable<string> FilterTerms => Keywords == null ? new[] { LabelText } : new List<string>(Keywords) { LabelText }.ToArray(); + public virtual IEnumerable<string> FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List<string>(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable<string> Keywords { get; set; } @@ -120,8 +121,10 @@ namespace osu.Game.Overlays.Settings labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } - private class RestoreDefaultValueButton : Container, IHasTooltip + protected internal class RestoreDefaultValueButton : Container, IHasTooltip { + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + private Bindable<T> bindable; public Bindable<T> Bindable @@ -206,7 +209,9 @@ namespace osu.Game.Overlays.Settings UpdateState(); } - public void UpdateState() + public void UpdateState() => Scheduler.AddOnce(updateState); + + private void updateState() { if (bindable == null) return; diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 1b82d973e9..6abf6283b9 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -8,10 +8,12 @@ using osu.Game.Graphics.Sprites; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Testing; using osu.Game.Graphics; namespace osu.Game.Overlays.Settings { + [ExcludeFromDynamicCompile] public abstract class SettingsSubsection : FillFlowContainer, IHasFilterableChildren { protected override Container<Drawable> Content => FlowContent; diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index f1270f750e..8f3274b2b5 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays private const float sidebar_width = Sidebar.DEFAULT_WIDTH; - protected const float WIDTH = 400; + public const float WIDTH = 400; protected Container<Drawable> ContentContainer; diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 393e349bd0..d049c2d3ec 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -13,14 +13,22 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Events; using osu.Game.Rulesets; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { - public class Toolbar : VisibilityContainer + public class Toolbar : VisibilityContainer, IKeyBindingHandler<GlobalAction> { public const float HEIGHT = 40; public const float TOOLTIP_HEIGHT = 30; + /// <summary> + /// Whether the user hid this <see cref="Toolbar"/> with <see cref="GlobalAction.ToggleToolbar"/>. + /// In this state, automatic toggles should not occur, respecting the user's preference to have no toolbar. + /// </summary> + private bool hiddenByUser; + public Action OnHome; private ToolbarUserButton userButton; @@ -30,13 +38,22 @@ namespace osu.Game.Overlays.Toolbar protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); - // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. + // Toolbar and its components need keyboard input even when hidden. public override bool PropagateNonPositionalInputSubTree => true; public Toolbar() { RelativeSizeAxes = Axes.X; Size = new Vector2(1, HEIGHT); + AlwaysPresent = true; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // this only needed to be set for the initial LoadComplete/Update, so layout completes and gets buttons in a state they can correctly handle keyboard input for hotkeys. + AlwaysPresent = false; } [BackgroundDependencyLoader(true)] @@ -133,7 +150,9 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateState(ValueChangedEvent<Visibility> state) { - if (state.NewValue == Visibility.Visible && OverlayActivationMode.Value == OverlayActivation.Disabled) + bool blockShow = hiddenByUser || OverlayActivationMode.Value == OverlayActivation.Disabled; + + if (state.NewValue == Visibility.Visible && blockShow) { State.Value = Visibility.Hidden; return; @@ -155,5 +174,25 @@ namespace osu.Game.Overlays.Toolbar this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint); this.FadeOut(transition_time, Easing.InQuint); } + + public bool OnPressed(GlobalAction action) + { + if (OverlayActivationMode.Value == OverlayActivation.Disabled) + return false; + + switch (action) + { + case GlobalAction.ToggleToolbar: + hiddenByUser = State.Value == Visibility.Visible; // set before toggling to allow the operation to always succeed. + ToggleVisibility(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 83f2bdf6cb..1933422dd9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -43,19 +45,19 @@ namespace osu.Game.Overlays.Toolbar Texture = textures.Get(texture), }); - public string Text + public LocalisableString Text { get => DrawableText.Text; set => DrawableText.Text = value; } - public string TooltipMain + public LocalisableString TooltipMain { get => tooltip1.Text; set => tooltip1.Text = value; } - public string TooltipSub + public LocalisableString TooltipSub { get => tooltip2.Text; set => tooltip2.Text = value; @@ -127,9 +129,9 @@ namespace osu.Game.Overlays.Toolbar { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize - Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, + Anchor = TooltipAnchor.HasFlagFast(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, Origin = TooltipAnchor, - Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5), + Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5), Alpha = 0, Children = new Drawable[] { diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index a4179c94da..6f979b8dc8 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; +using osu.Game.Screens; using osu.Game.Screens.Menu; namespace osu.Game @@ -29,9 +28,6 @@ namespace osu.Game [Resolved] private DialogOverlay dialogOverlay { get; set; } - [Resolved] - private IBindable<WorkingBeatmap> beatmap { get; set; } - [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -81,27 +77,45 @@ namespace osu.Game game?.CloseAllOverlays(false); - // we may already be at the target screen type. + findValidTarget(current); + } + + private bool findValidTarget(IScreen current) + { var type = current.GetType(); - if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled) + // check if we are already at a valid target screen. + if (validScreens.Any(t => t.IsAssignableFrom(type))) { finalAction(current); Cancel(); - return; + return true; } while (current != null) { + // if this has a sub stack, recursively check the screens within it. + if (current is IHasSubScreenStack currentSubScreen) + { + if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen)) + { + // should be correct in theory, but currently untested/unused in existing implementations. + current.MakeCurrent(); + return true; + } + } + if (validScreens.Any(t => t.IsAssignableFrom(type))) { current.MakeCurrent(); - break; + return true; } current = current.GetParentScreen(); type = current?.GetType(); } + + return false; } /// <summary> diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index ab9ccda9b9..f6abf259e8 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -3,6 +3,7 @@ using MessagePack; using Newtonsoft.Json; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Replays; using osuTK; @@ -31,19 +32,19 @@ namespace osu.Game.Replays.Legacy [JsonIgnore] [IgnoreMember] - public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); + public bool MouseLeft1 => ButtonState.HasFlagFast(ReplayButtonState.Left1); [JsonIgnore] [IgnoreMember] - public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); + public bool MouseRight1 => ButtonState.HasFlagFast(ReplayButtonState.Right1); [JsonIgnore] [IgnoreMember] - public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); + public bool MouseLeft2 => ButtonState.HasFlagFast(ReplayButtonState.Left2); [JsonIgnore] [IgnoreMember] - public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); + public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); [Key(3)] public ReplayButtonState ButtonState; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f15e5e1df0..a25dc3e6db 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Difficulty private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) { - var skills = CreateSkills(beatmap); + var skills = CreateSkills(beatmap, mods); if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); @@ -202,7 +202,8 @@ namespace osu.Game.Rulesets.Difficulty /// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>. /// </summary> /// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param> + /// <param name="mods">Mods to calculate difficulty with.</param> /// <returns>The <see cref="Skill"/>s.</returns> - protected abstract Skill[] CreateSkills(IBeatmap beatmap); + protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods); } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 1063a24b27..95117be073 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty.Skills { @@ -46,10 +47,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// </summary> protected double CurrentStrain { get; private set; } = 1; + /// <summary> + /// Mods for use in skill calculations. + /// </summary> + protected IReadOnlyList<Mod> Mods => mods; + private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. private readonly List<double> strainPeaks = new List<double>(); + private readonly Mod[] mods; + + protected Skill(Mod[] mods) + { + this.mods = mods; + } + /// <summary> /// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly. /// </summary> diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index e927951d0a..736fc47dee 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -438,6 +438,8 @@ namespace osu.Game.Rulesets.Edit /// </summary> public abstract bool CursorInPlacementArea { get; } + public virtual string ConvertSelectionToString() => string.Empty; + #region IPositionSnapProvider public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs new file mode 100644 index 0000000000..13cc41f8e0 --- /dev/null +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Filter +{ + /// <summary> + /// Allows for extending the beatmap filtering capabilities of song select (as implemented in <see cref="FilterCriteria"/>) + /// with ruleset-specific criteria. + /// </summary> + public interface IRulesetFilterCriteria + { + /// <summary> + /// Checks whether the supplied <paramref name="beatmap"/> satisfies ruleset-specific custom criteria, + /// in addition to the ones mandated by song select. + /// </summary> + /// <param name="beatmap">The beatmap to test the criteria against.</param> + /// <returns> + /// <c>true</c> if the beatmap matches the ruleset-specific custom filtering criteria, + /// <c>false</c> otherwise. + /// </returns> + bool Matches(BeatmapInfo beatmap); + + /// <summary> + /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box. + /// The format of the criterion is: + /// <code> + /// {key}{op}{value} + /// </code> + /// </summary> + /// <remarks> + /// <para> + /// For adding optional string criteria, <see cref="FilterCriteria.OptionalTextFilter"/> can be used for matching, + /// along with <see cref="FilterQueryParser.TryUpdateCriteriaText"/> for parsing. + /// </para> + /// <para> + /// For adding numerical-type range criteria, <see cref="FilterCriteria.OptionalRange{T}"/> can be used for matching, + /// along with <see cref="FilterQueryParser.TryUpdateCriteriaRange{T}(ref osu.Game.Screens.Select.FilterCriteria.OptionalRange{T},osu.Game.Screens.Select.Filter.Operator,string,FilterQueryParser.TryParseFunction{T})"/> + /// and <see cref="float"/>- and <see cref="double"/>-typed overloads for parsing. + /// </para> + /// </remarks> + /// <param name="key">The key (name) of the criterion.</param> + /// <param name="op">The operator in the criterion.</param> + /// <param name="value">The value of the criterion.</param> + /// <returns> + /// <c>true</c> if the keyword criterion is valid, <c>false</c> if it has been ignored. + /// Valid criteria are stripped from <see cref="FilterCriteria.SearchText"/>, + /// while ignored criteria are included in <see cref="FilterCriteria.SearchText"/>. + /// </returns> + bool TryParseCustomKeywordCriteria(string key, Operator op, string value); + } +} diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index da9bb8a09d..feeafb7151 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -150,17 +150,13 @@ namespace osu.Game.Rulesets.Judgements } if (JudgementBody.Drawable is IAnimatableJudgement animatable) - { - var drawableAnimation = (Drawable)animatable; - animatable.PlayAnimation(); - // a derived version of DrawableJudgement may be proposing a lifetime. - // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. - double lastTransformTime = drawableAnimation.LatestTransformEndTime; - if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) - LifetimeEnd = lastTransformTime; - } + // a derived version of DrawableJudgement may be proposing a lifetime. + // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. + double lastTransformTime = JudgementBody.Drawable.LatestTransformEndTime; + if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) + LifetimeEnd = lastTransformTime; } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index d1d23def67..d6e1d46b06 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs new file mode 100644 index 0000000000..c0d7bae2b2 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride + { + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + + public virtual bool PerformFail() => true; + + public virtual bool RestartOnFail => true; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + healthProcessor.FailConditions += FailCondition; + } + + protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result); + } +} diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index b95ec7490e..c0f24e116a 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods public override string Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) }; } } diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index df0fc9c4b6..d0b09b50f2 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -8,13 +10,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModPerfect : ModSuddenDeath + public abstract class ModPerfect : ModFailCondition { public override string Name => "Perfect"; public override string Acronym => "PF"; public override IconUsage? Icon => OsuIcon.ModPerfect; + public override ModType Type => ModType.DifficultyIncrease; + public override bool Ranked => true; + public override double ScoreMultiplier => 1; public override string Description => "SS or quit."; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type.AffectsAccuracy() && result.Type != result.Judgement.MaxResult; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index b016a6d43b..e66650f7b4 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Mods public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value; + public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) }; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index b6fec42f43..e5995ff180 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModSuddenDeath) }; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index ae71041a64..617ae38feb 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -9,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModSuddenDeath : Mod, IApplicableToHealthProcessor, IApplicableFailOverride + public abstract class ModSuddenDeath : ModFailCondition { public override string Name => "Sudden Death"; public override string Acronym => "SD"; @@ -18,18 +19,10 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Miss and fail."; public override double ScoreMultiplier => 1; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; - public bool PerformFail() => true; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); - public bool RestartOnFail => true; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - healthProcessor.FailConditions += FailCondition; - } - - protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type.AffectsCombo() && !result.IsHit; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 330945d3d3..b5cd64dafa 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust) }; + public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; private double finalRateTime; diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 679b50057b..08bd44f7bd 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Initial rate", "The starting speed of the track")] public override BindableNumber<double> InitialRate { get; } = new BindableDouble { - MinValue = 1, + MinValue = 0.51, MaxValue = 2, Default = 1, Value = 1, @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override BindableNumber<double> FinalRate { get; } = new BindableDouble { MinValue = 0.5, - MaxValue = 0.99, + MaxValue = 1.99, Default = 0.75, Value = 0.75, Precision = 0.01, @@ -45,5 +45,20 @@ namespace osu.Game.Rulesets.Mods }; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); + + public ModWindDown() + { + InitialRate.BindValueChanged(val => + { + if (val.NewValue <= FinalRate.Value) + FinalRate.Value = val.NewValue - FinalRate.Precision; + }); + + FinalRate.BindValueChanged(val => + { + if (val.NewValue >= InitialRate.Value) + InitialRate.Value = val.NewValue + InitialRate.Precision; + }); + } } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index b733bf423e..df8f781148 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override BindableNumber<double> InitialRate { get; } = new BindableDouble { MinValue = 0.5, - MaxValue = 1, + MaxValue = 1.99, Default = 1, Value = 1, Precision = 0.01, @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The speed increase to ramp towards")] public override BindableNumber<double> FinalRate { get; } = new BindableDouble { - MinValue = 1.01, + MinValue = 0.51, MaxValue = 2, Default = 1.5, Value = 1.5, @@ -45,5 +45,20 @@ namespace osu.Game.Rulesets.Mods }; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); + + public ModWindUp() + { + InitialRate.BindValueChanged(val => + { + if (val.NewValue >= FinalRate.Value) + FinalRate.Value = val.NewValue + FinalRate.Precision; + }); + + FinalRate.BindValueChanged(val => + { + if (val.NewValue <= InitialRate.Value) + InitialRate.Value = val.NewValue - InitialRate.Precision; + }); + } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 72025de131..8419dd66de 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; using osu.Game.Skinning; @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; type &= ~LegacyHitObjectType.ComboOffset; - bool combo = type.HasFlag(LegacyHitObjectType.NewCombo); + bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo); type &= ~LegacyHitObjectType.NewCombo; var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); @@ -62,14 +63,14 @@ namespace osu.Game.Rulesets.Objects.Legacy HitObject result = null; - if (type.HasFlag(LegacyHitObjectType.Circle)) + if (type.HasFlagFast(LegacyHitObjectType.Circle)) { result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); } - else if (type.HasFlag(LegacyHitObjectType.Slider)) + else if (type.HasFlagFast(LegacyHitObjectType.Slider)) { double? length = null; @@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } - else if (type.HasFlag(LegacyHitObjectType.Spinner)) + else if (type.HasFlagFast(LegacyHitObjectType.Spinner)) { double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); @@ -150,7 +151,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); } - else if (type.HasFlag(LegacyHitObjectType.Hold)) + else if (type.HasFlagFast(LegacyHitObjectType.Hold)) { // Note: Hold is generated by BMS converts @@ -436,16 +437,16 @@ namespace osu.Game.Rulesets.Objects.Legacy new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds - type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)) + type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)) }; - if (type.HasFlag(LegacyHitSoundType.Finish)) + if (type.HasFlagFast(LegacyHitSoundType.Finish)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlag(LegacyHitSoundType.Whistle)) + if (type.HasFlagFast(LegacyHitSoundType.Whistle)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlag(LegacyHitSoundType.Clap)) + if (type.HasFlagFast(LegacyHitSoundType.Clap)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); return soundTypes; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index dbc2bd4d01..38d30a2e31 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -26,6 +26,7 @@ using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Testing; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets @@ -306,5 +307,11 @@ namespace osu.Game.Rulesets /// <param name="result">The result type to get the name for.</param> /// <returns>The display name.</returns> public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); + + /// <summary> + /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. + /// </summary> + [CanBeNull] + public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 2024290460..b81fa79345 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -337,7 +337,7 @@ namespace osu.Game.Rulesets.Scoring score.TotalScore = (long)Math.Round(GetStandardisedScore()); score.Combo = Combo.Value; score.MaxCombo = HighestCombo.Value; - score.Accuracy = Math.Round(Accuracy.Value, 4); + score.Accuracy = Accuracy.Value; score.Rank = Rank.Value; score.Date = DateTimeOffset.Now; diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index bbaca7c80f..e66a8c016c 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.UI ~DrawableRulesetDependencies() { + // required to potentially clean up sample store from audio hierarchy. Dispose(false); } @@ -116,6 +117,10 @@ namespace osu.Game.Rulesets.UI public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); + public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + + public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + public BindableNumber<double> Volume => throw new NotSupportedException(); public BindableNumber<double> Balance => throw new NotSupportedException(); @@ -124,8 +129,6 @@ namespace osu.Game.Rulesets.UI public BindableNumber<double> Tempo => throw new NotSupportedException(); - public IBindable<double> GetAggregate(AdjustableProperty type) => throw new NotSupportedException(); - public IBindable<double> AggregateVolume => throw new NotSupportedException(); public IBindable<double> AggregateBalance => throw new NotSupportedException(); diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 07de2bf601..d6f002ea2c 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -16,7 +16,6 @@ using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; -using osuTK.Input; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI @@ -109,9 +108,9 @@ namespace osu.Game.Rulesets.UI { switch (e) { - case MouseDownEvent mouseDown when mouseDown.Button == MouseButton.Left || mouseDown.Button == MouseButton.Right: + case MouseDownEvent _: if (mouseDisabled.Value) - return false; + return true; // importantly, block upwards propagation so global bindings also don't fire. break; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index f5192f3a40..ef11c19e3f 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -30,7 +30,7 @@ namespace osu.Game.Scoring public long TotalScore { get; set; } [JsonProperty("accuracy")] - [Column(TypeName = "DECIMAL(1,4)")] + [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database. public double Accuracy { get; set; } [JsonIgnore] diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index a6beb19876..c7ee26c248 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -20,6 +20,7 @@ using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring.Legacy; @@ -137,7 +138,7 @@ namespace osu.Game.Scoring ScoringMode.BindValueChanged(onScoringModeChanged, true); } - private IBindable<StarDifficulty> difficultyBindable; + private IBindable<StarDifficulty?> difficultyBindable; private CancellationTokenSource difficultyCancellationSource; private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode) @@ -152,9 +153,21 @@ namespace osu.Game.Scoring } int beatmapMaxCombo; + double accuracy = score.Accuracy; if (score.IsLegacyScore) { + if (score.RulesetID == 3) + { + // In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score. + // To get around this, recalculate accuracy based on the hit statistics. + // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. + double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect); + double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum(); + if (maxBaseScore > 0) + accuracy = baseScore / maxBaseScore; + } + // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. if (score.Beatmap.MaxCombo == null) @@ -168,7 +181,11 @@ namespace osu.Game.Scoring // We can compute the max combo locally after the async beatmap difficulty computation. difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); - difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); + difficultyBindable.BindValueChanged(d => + { + if (d.NewValue is StarDifficulty diff) + updateScore(diff.MaxCombo, accuracy); + }, true); return; } @@ -182,10 +199,10 @@ namespace osu.Game.Scoring beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum(); } - updateScore(beatmapMaxCombo); + updateScore(beatmapMaxCombo, accuracy); } - private void updateScore(int beatmapMaxCombo) + private void updateScore(int beatmapMaxCombo, double accuracy) { if (beatmapMaxCombo == 0) { @@ -198,7 +215,7 @@ namespace osu.Game.Scoring scoreProcessor.Mods.Value = score.Mods; - Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); + Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); } } diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index 5f66c13d2f..bb15983de3 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); + var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index 48c5523883..a6fb94b151 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens { private readonly bool animateOnEnter; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + protected BackgroundScreen(bool animateOnEnter = true) { this.animateOnEnter = animateOnEnter; diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index d9477dd4bc..ff33f0c70d 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics.Colour; using osu.Game.Graphics; using osuTK.Graphics; @@ -48,7 +47,7 @@ namespace osu.Game.Screens.Edit /// <param name="beatDivisor">The beat divisor.</param> /// <param name="colours">The set of colours.</param> /// <returns>The applicable colour from <paramref name="colours"/> for <paramref name="beatDivisor"/>.</returns> - public static ColourInfo GetColourFor(int beatDivisor, OsuColour colours) + public static Color4 GetColourFor(int beatDivisor, OsuColour colours) { switch (beatDivisor) { diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 9739f2876a..bdc6e238c8 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Components [Resolved] private EditorClock editorClock { get; set; } - private readonly BindableNumber<double> tempo = new BindableDouble(1); + private readonly BindableNumber<double> freqAdjust = new BindableDouble(1); [BackgroundDependencyLoader] private void load() @@ -58,16 +58,16 @@ namespace osu.Game.Screens.Edit.Components RelativeSizeAxes = Axes.Both, Height = 0.5f, Padding = new MarginPadding { Left = 45 }, - Child = new PlaybackTabControl { Current = tempo }, + Child = new PlaybackTabControl { Current = freqAdjust }, } }; - Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true); + Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true); } protected override void Dispose(bool isDisposing) { - Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust); base.Dispose(isDisposing); } @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Components private class PlaybackTabControl : OsuTabControl<double> { - private static readonly double[] tempo_values = { 0.5, 0.75, 1 }; + private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 }; protected override TabItem<double> CreateTabItem(double value) => new PlaybackTabItem(value); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5371beac60..5699be4560 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -268,6 +268,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addBlueprintFor(HitObject hitObject) { + if (hitObject is IBarLine) + return; + if (blueprintMap.ContainsKey(hitObject)) return; @@ -338,7 +341,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool beginClickSelection(MouseButtonEvent e) { // Iterate from the top of the input stack (blueprints closest to the front of the screen first). - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse()) + // Priority is given to already-selected blueprints. + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) { if (!blueprint.IsHovered) continue; @@ -495,8 +499,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Apply the start time at the newly snapped-to position double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; - foreach (HitObject obj in Beatmap.SelectedHitObjects) - obj.StartTime += offset; + Beatmap.PerformOnSelection(obj => obj.StartTime += offset); } return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 8a92a2011d..59f88ac641 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); int repeatIndex = placementIndex / beatDivisor.Value; - return colour.MultiplyAlpha(0.5f / (repeatIndex + 1)); + return ColourInfo.SingleColour(colour).MultiplyAlpha(0.5f / (repeatIndex + 1)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 9e95fe4fa1..d612cf3fe0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -71,7 +71,13 @@ namespace osu.Game.Screens.Edit.Compose.Components // Put earlier blueprints towards the end of the list, so they handle input first int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); - return i == 0 ? CompareReverseChildID(x, y) : i; + + if (i != 0) return i; + + // Fall back to end time if the start time is equal. + i = yObj.HitObject.GetEndTime().CompareTo(xObj.HitObject.GetEndTime()); + + return i == 0 ? CompareReverseChildID(y, x) : i; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 2f4721f63e..9d6b44e207 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -113,16 +113,25 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Repeat || !e.ControlPressed) return false; + bool runOperationFromHotkey(Func<bool> operation) + { + operationStarted(); + bool result = operation?.Invoke() ?? false; + operationEnded(); + + return result; + } + switch (e.Key) { case Key.G: - return CanReverse && OnReverse?.Invoke() == true; + return CanReverse && runOperationFromHotkey(OnReverse); case Key.H: - return CanScaleX && OnFlip?.Invoke(Direction.Horizontal) == true; + return CanScaleX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false); case Key.J: - return CanScaleY && OnFlip?.Invoke(Direction.Vertical) == true; + return CanScaleY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false); } return base.OnKeyDown(e); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 788b485449..018d4d081c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -320,18 +320,14 @@ namespace osu.Game.Screens.Edit.Compose.Components /// <param name="sampleName">The name of the hit sample.</param> public void AddHitSample(string sampleName) { - EditorBeatmap.BeginChange(); - - foreach (var h in EditorBeatmap.SelectedHitObjects) + EditorBeatmap.PerformOnSelection(h => { // Make sure there isn't already an existing sample if (h.Samples.Any(s => s.Name == sampleName)) - continue; + return; h.Samples.Add(new HitSampleInfo(sampleName)); - } - - EditorBeatmap.EndChange(); + }); } /// <summary> @@ -341,19 +337,15 @@ namespace osu.Game.Screens.Edit.Compose.Components /// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception> public void SetNewCombo(bool state) { - EditorBeatmap.BeginChange(); - - foreach (var h in EditorBeatmap.SelectedHitObjects) + EditorBeatmap.PerformOnSelection(h => { var comboInfo = h as IHasComboInformation; - if (comboInfo == null || comboInfo.NewCombo == state) continue; + if (comboInfo == null || comboInfo.NewCombo == state) return; comboInfo.NewCombo = state; EditorBeatmap.Update(h); - } - - EditorBeatmap.EndChange(); + }); } /// <summary> @@ -362,12 +354,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// <param name="sampleName">The name of the hit sample.</param> public void RemoveHitSample(string sampleName) { - EditorBeatmap.BeginChange(); - - foreach (var h in EditorBeatmap.SelectedHitObjects) - h.SamplesBindable.RemoveAll(s => s.Name == sampleName); - - EditorBeatmap.EndChange(); + EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName)); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 666026e05e..0697dbb392 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -177,7 +177,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!track.IsLoaded) return; - editorClock.Seek(Current / Content.DrawWidth * track.Length); + double target = Current / Content.DrawWidth * track.Length; + editorClock.Seek(Math.Min(track.Length, target)); } private void scrollToTrackTime() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 1fc529910b..3623f8ad8e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -121,14 +123,55 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } base.Update(); + + updateStacking(); + } + + private void updateStacking() + { + // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. + + const int stack_offset = 5; + + // after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints. + const int stack_reset_count = 3; + + Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>(); + + foreach (var b in SelectionBlueprints.Reverse()) + { + // remove objects from the stack as long as their end time is in the past. + while (currentConcurrentObjects.TryPeek(out HitObject hitObject)) + { + if (Precision.AlmostBigger(hitObject.GetEndTime(), b.HitObject.StartTime, 1)) + break; + + currentConcurrentObjects.Pop(); + } + + // if the stack gets too high, we should have space below it to display the next batch of objects. + // importantly, we only do this if time has incremented, else a stack of hitobjects all at the same time value would start to overlap themselves. + if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.HitObject.StartTime, 1)) + { + if (currentConcurrentObjects.Count >= stack_reset_count) + currentConcurrentObjects.Clear(); + } + + b.Y = -(stack_offset * currentConcurrentObjects.Count); + + currentConcurrentObjects.Push(b.HitObject); + } } protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler(); - protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject) + protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - OnDragHandled = handleScrollViaDrag - }; + return new TimelineHitObjectBlueprint(hitObject) + { + OnDragHandled = handleScrollViaDrag + }; + } protected override DragBox CreateDragBox(Action<RectangleF> performSelect) => new TimelineDragBox(performSelect); @@ -203,7 +246,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Box.X = Math.Min(rescaledStart, rescaledEnd); Box.Width = Math.Abs(rescaledStart - rescaledEnd); - PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat); + var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat; + + // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment. + boxScreenRect.Y -= boxScreenRect.Height; + boxScreenRect.Height *= 2; + + PerformSelection?.Invoke(boxScreenRect); } public override void Hide() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index fb11b859a7..c070c833f8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -6,7 +6,9 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -124,25 +126,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (beat == 0 && i == 0) nextMinTick = float.MinValue; - var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value); + int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value); var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var colour = BindableBeatDivisor.GetColourFor(divisor, colours); + bool isMainBeat = indexInBar == 0; + // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. - var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f; + float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f; + float gradientOpacity = isMainBeat ? 1 : 0; var topPoint = getNextUsablePoint(); topPoint.X = xPos; - topPoint.Colour = colour; topPoint.Height = height; + topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity)); topPoint.Anchor = Anchor.TopLeft; topPoint.Origin = Anchor.TopCentre; var bottomPoint = getNextUsablePoint(); bottomPoint.X = xPos; - bottomPoint.Colour = colour; bottomPoint.Anchor = Anchor.BottomLeft; + bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour); bottomPoint.Origin = Anchor.BottomCentre; bottomPoint.Height = height; } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 81b1195a40..61056aeced 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -2,11 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -14,11 +19,17 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Edit.Compose { - public class ComposeScreen : EditorScreenWithTimeline + public class ComposeScreen : EditorScreenWithTimeline, IKeyBindingHandler<PlatformAction> { [Resolved] private IBindable<WorkingBeatmap> beatmap { get; set; } + [Resolved] + private GameHost host { get; set; } + + [Resolved] + private EditorClock clock { get; set; } + private HitObjectComposer composer; public ComposeScreen() @@ -72,5 +83,34 @@ namespace osu.Game.Screens.Edit.Compose // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content)); } + + #region Input Handling + + public bool OnPressed(PlatformAction action) + { + if (action.ActionType == PlatformActionType.Copy) + host.GetClipboard().SetText(formatSelectionAsString()); + + return false; + } + + public void OnReleased(PlatformAction action) + { + } + + private string formatSelectionAsString() + { + if (composer == null) + return string.Empty; + + double displayTime = EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).FirstOrDefault()?.StartTime ?? clock.CurrentTime; + string selectionAsString = composer.ConvertSelectionToString(); + + return !string.IsNullOrEmpty(selectionAsString) + ? $"{displayTime.ToEditorFormattedString()} ({selectionAsString}) - " + : $"{displayTime.ToEditorFormattedString()} - "; + } + + #endregion } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0ba202b082..389eb79797 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -16,7 +16,6 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -74,7 +73,6 @@ namespace osu.Game.Screens.Edit private string lastSavedHash; - private Box bottomBackground; private Container<EditorScreen> screenContainer; private EditorScreen currentScreen; @@ -104,28 +102,31 @@ namespace osu.Game.Screens.Edit private MusicController music { get; set; } [BackgroundDependencyLoader] - private void load(OsuColour colours, GameHost host, OsuConfigManager config) + private void load(OsuColour colours, OsuConfigManager config) { - if (Beatmap.Value is DummyWorkingBeatmap) + var loadableBeatmap = Beatmap.Value; + + if (loadableBeatmap is DummyWorkingBeatmap) { isNewBeatmap = true; - var newBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + + // required so we can get the track length in EditorClock. + // this is safe as nothing has yet got a reference to this new beatmap. + loadableBeatmap.LoadTrack(); // this is a bit haphazard, but guards against setting the lease Beatmap bindable if // the editor has already been exited. if (!ValidForPush) return; - - // this probably shouldn't be set in the asynchronous load method, but everything following relies on it. - Beatmap.Value = newBeatmap; } - beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; - beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); + beatDivisor.Value = loadableBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.BindValueChanged(divisor => loadableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(loadableBeatmap, beatDivisor) { IsCoupled = false }; UpdateClockSource(); @@ -139,7 +140,7 @@ namespace osu.Game.Screens.Edit try { - playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + playableBeatmap = loadableBeatmap.GetPlayableBeatmap(loadableBeatmap.BeatmapInfo.Ruleset); // clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages. // eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases. @@ -153,13 +154,21 @@ namespace osu.Game.Screens.Edit return; } - AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, Beatmap.Value.Skin)); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.Skin)); dependencies.CacheAs(editorBeatmap); changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs<IEditorChangeHandler>(changeHandler); updateLastSavedHash(); + Schedule(() => + { + // we need to avoid changing the beatmap from an asynchronous load thread. it can potentially cause weirdness including crashes. + // this assumes that nothing during the rest of this load() method is accessing Beatmap.Value (loadableBeatmap should be preferred). + // generally this is quite safe, as the actual load of editor content comes after menuBar.Mode.ValueChanged is fired in its own LoadComplete. + Beatmap.Value = loadableBeatmap; + }); + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -167,17 +176,6 @@ namespace osu.Game.Screens.Edit EditorMenuItem copyMenuItem; EditorMenuItem pasteMenuItem; - var fileMenuItems = new List<MenuItem> - { - new EditorMenuItem("Save", MenuItemType.Standard, Save) - }; - - if (RuntimeInfo.IsDesktop) - fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap)); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); - AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -209,7 +207,7 @@ namespace osu.Game.Screens.Edit { new MenuItem("File") { - Items = fileMenuItems + Items = createFileMenuItems() }, new MenuItem("Edit") { @@ -242,7 +240,11 @@ namespace osu.Game.Screens.Edit Height = 60, Children = new Drawable[] { - bottomBackground = new Box { RelativeSizeAxes = Axes.Both }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray2 + }, new Container { RelativeSizeAxes = Axes.Both, @@ -299,8 +301,6 @@ namespace osu.Game.Screens.Edit clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue)); menuBar.Mode.ValueChanged += onModeChanged; - - bottomBackground.Colour = colours.Gray2; } /// <summary> @@ -681,6 +681,21 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler.CurrentStateHash; } + private List<MenuItem> createFileMenuItems() + { + var fileMenuItems = new List<MenuItem> + { + new EditorMenuItem("Save", MenuItemType.Standard, Save) + }; + + if (RuntimeInfo.IsDesktop) + fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap)); + + fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); + return fileMenuItems; + } + public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 174ff1478b..4f1b0484d2 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -100,6 +100,22 @@ namespace osu.Game.Screens.Edit private readonly HashSet<HitObject> batchPendingUpdates = new HashSet<HitObject>(); + /// <summary> + /// Perform the provided action on every selected hitobject. + /// Changes will be grouped as one history action. + /// </summary> + /// <param name="action">The action to perform.</param> + public void PerformOnSelection(Action<HitObject> action) + { + if (SelectedHitObjects.Count == 0) + return; + + BeginChange(); + foreach (var h in SelectedHitObjects) + action(h); + EndChange(); + } + /// <summary> /// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>. /// </summary> diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index ec0f5d7154..d0197ce1ec 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; + private bool playbackFinished; + public IBindable<bool> SeekingOrStopped => seekingOrStopped; private readonly Bindable<bool> seekingOrStopped = new Bindable<bool>(true); @@ -170,6 +172,10 @@ namespace osu.Game.Screens.Edit public void Start() { ClearTransforms(); + + if (playbackFinished) + underlyingClock.Seek(0); + underlyingClock.Start(); } @@ -216,7 +222,21 @@ namespace osu.Game.Screens.Edit public bool IsRunning => underlyingClock.IsRunning; - public void ProcessFrame() => underlyingClock.ProcessFrame(); + public void ProcessFrame() + { + underlyingClock.ProcessFrame(); + + playbackFinished = CurrentTime >= TrackLength; + + if (playbackFinished) + { + if (IsRunning) + underlyingClock.Stop(); + + if (CurrentTime > TrackLength) + underlyingClock.Seek(TrackLength); + } + } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index e812c042fb..f429164ece 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -56,6 +56,14 @@ namespace osu.Game.Screens.Edit.Setup item.OnCommit += onCommit; } + protected override void LoadComplete() + { + base.LoadComplete(); + + if (string.IsNullOrEmpty(artistTextBox.Current.Value)) + GetContainingInputManager().ChangeFocus(artistTextBox); + } + private void onCommit(TextBox sender, bool newText) { if (!newText) return; diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index b87b8961f8..9d80ca0b14 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Timing { multiplierSlider = new SliderWithTextBoxInput<double>("Speed Multiplier") { - Current = new DifficultyControlPoint().SpeedMultiplierBindable + Current = new DifficultyControlPoint().SpeedMultiplierBindable, + KeyboardStep = 0.1f } }); } diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index f2f9f76143..10a5771520 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -69,6 +69,15 @@ namespace osu.Game.Screens.Edit.Timing }, true); } + /// <summary> + /// A custom step value for each key press which actuates a change on this control. + /// </summary> + public float KeyboardStep + { + get => slider.KeyboardStep; + set => slider.KeyboardStep = value; + } + public Bindable<T> Current { get => slider.Current; diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs new file mode 100644 index 0000000000..60801fb3eb --- /dev/null +++ b/osu.Game/Screens/IHandlePresentBeatmap.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Screens +{ + /// <summary> + /// Denotes a screen which can handle beatmap / ruleset selection via local logic. + /// This is used in the <see cref="OsuGame.PresentBeatmap"/> flow to handle cases which require custom logic, + /// for instance, if a lease is held on the Beatmap. + /// </summary> + public interface IHandlePresentBeatmap + { + /// <summary> + /// Invoked with a requested beatmap / ruleset for selection. + /// </summary> + /// <param name="beatmap">The beatmap to be selected.</param> + /// <param name="ruleset">The ruleset to be selected.</param> + void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset); + } +} diff --git a/osu.Game/Screens/IHasSubScreenStack.cs b/osu.Game/Screens/IHasSubScreenStack.cs new file mode 100644 index 0000000000..c5e2015109 --- /dev/null +++ b/osu.Game/Screens/IHasSubScreenStack.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Screens; + +namespace osu.Game.Screens +{ + /// <summary> + /// A screen which manages a nested stack of screens within itself. + /// </summary> + public interface IHasSubScreenStack + { + ScreenStack SubScreenStack { get; } + } +} diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 329623e03a..ee8ef6926d 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Import Task.Factory.StartNew(async () => { - await game.Import(path); + await game.Import(path).ConfigureAwait(false); // some files will be deleted after successful import, so we want to refresh the view. Schedule(() => diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index d120eb21a8..6488a2fd63 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -9,10 +9,15 @@ namespace osu.Game.Screens.Menu { public class ConfirmExitDialog : PopupDialog { - public ConfirmExitDialog(Action confirm, Action cancel) + /// <summary> + /// Construct a new exit confirmation dialog. + /// </summary> + /// <param name="onConfirm">An action to perform on confirmation.</param> + /// <param name="onCancel">An optional action to perform on cancel.</param> + public ConfirmExitDialog(Action onConfirm, Action onCancel = null) { - HeaderText = "Are you sure you want to exit?"; - BodyText = "Last chance to back out."; + HeaderText = "Are you sure you want to exit osu!?"; + BodyText = "Last chance to turn back"; Icon = FontAwesome.Solid.ExclamationTriangle; @@ -20,13 +25,13 @@ namespace osu.Game.Screens.Menu { new PopupDialogOkButton { - Text = @"Goodbye", - Action = confirm + Text = @"Let me out!", + Action = onConfirm }, new PopupDialogCancelButton { - Text = @"Just a little more", - Action = cancel + Text = @"Just a little more...", + Action = onCancel }, }; } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index ffe6882a72..abe6c62461 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Menu rulesets.Hide(); lazerLogo.Hide(); - background.Hide(); + background.ApplyToBackground(b => b.Hide()); using (BeginAbsoluteSequence(0, true)) { @@ -231,7 +231,8 @@ namespace osu.Game.Screens.Menu lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run. logo.FadeIn(); - background.FadeIn(); + + background.ApplyToBackground(b => b.Show()); game.Add(new GameWideFlash()); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 424e6d2cd5..baeb86c976 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -9,12 +9,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -23,7 +25,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu { - public class MainMenu : OsuScreen + public class MainMenu : OsuScreen, IHandlePresentBeatmap { public const float FADE_IN_DURATION = 300; @@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu Beatmap.SetDefault(); this.Push(new Editor()); }, - OnSolo = onSolo, + OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), OnExit = confirmAndExit, @@ -160,9 +162,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(songSelect = new PlaySongSelect()); } - public void LoadToSolo() => Schedule(onSolo); - - private void onSolo() => this.Push(consumeSongSelect()); + private void loadSoloSongSelect() => this.Push(consumeSongSelect()); private Screen consumeSongSelect() { @@ -289,5 +289,13 @@ namespace osu.Game.Screens.Menu this.FadeOut(3000); return base.OnExiting(next); } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + Beatmap.Value = beatmap; + Ruleset.Value = ruleset; + + Schedule(loadSoloSongSelect); + } } } diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index c4943e77d5..237fe43168 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -8,8 +8,8 @@ using osu.Game.Graphics.Sprites; using osuTK; using osu.Game.Graphics; using osu.Framework.Bindables; -using osu.Game.Beatmaps; using osu.Framework.Localisation; +using osu.Game.Beatmaps; namespace osu.Game.Screens.Menu { @@ -61,8 +61,8 @@ namespace osu.Game.Screens.Menu { var metadata = beatmap.Value.Metadata; - title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); - artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); + title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); + artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); this.FadeInFromZero(fade_duration / 2f) .Delay(4000) diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index acb82360b3..e5a5e35897 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { new OsuSpriteText { - Text = new LocalisedString((beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist)), + Text = new RomanisableString(beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist), Font = OsuFont.GetFont(size: TextSize), }, new OsuSpriteText @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Components }, new OsuSpriteText { - Text = new LocalisedString((beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title)), + Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title), Font = OsuFont.GetFont(size: TextSize), } }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index ab7be13479..66262e7dc4 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -75,6 +75,14 @@ namespace osu.Game.Screens.OnlinePlay section.DeselectAll(); } + protected override void OnAvailableModsChanged() + { + base.OnAvailableModsChanged(); + + foreach (var section in ModSectionsContainer.Children) + ((FreeModSection)section).UpdateCheckboxState(); + } + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection @@ -108,10 +116,14 @@ namespace osu.Game.Screens.OnlinePlay protected override void ModButtonStateChanged(Mod mod) { base.ModButtonStateChanged(mod); + UpdateCheckboxState(); + } + public void UpdateCheckboxState() + { if (!SelectionAnimationRunning) { - var validButtons = ButtonsContainer.OfType<ModButton>().Where(b => b.Mod.HasImplementation); + var validButtons = Buttons.Where(b => b.Mod.HasImplementation); checkbox.Current.Value = validButtons.All(b => b.Selected); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f13d623eae..f24577a8a5 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -170,7 +169,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void joinRequested(Room room) { - Debug.Assert(joiningRoomOperation == null); + if (joiningRoomOperation != null) + return; + joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); RoomManager?.JoinRoom(room, r => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index f17d97c3fd..c9f0f6de90 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -4,9 +4,11 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; @@ -19,6 +21,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer; + /// <summary> + /// Construct a new instance of multiplayer song select. + /// </summary> + /// <param name="beatmap">An optional initial beatmap selection to perform.</param> + /// <param name="ruleset">An optional initial ruleset selection to perform.</param> + public MultiplayerMatchSongSelect(WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) + { + if (beatmap != null || ruleset != null) + { + Schedule(() => + { + if (beatmap != null) Beatmap.Value = beatmap; + if (ruleset != null) Ruleset.Value = ruleset; + }); + } + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4fbea4e3be..ceeee67806 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -12,9 +12,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -29,7 +33,7 @@ using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.Pa namespace osu.Game.Screens.OnlinePlay.Multiplayer { [Cached] - public class MultiplayerMatchSubScreen : RoomSubScreen + public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap { public override string Title { get; } @@ -279,14 +283,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); } + [Resolved(canBeNull: true)] + private DialogOverlay dialogOverlay { get; set; } + + private bool exitConfirmed; + public override bool OnBackButton() { - if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible) + if (client.Room == null) + { + // room has not been created yet; exit immediately. + return base.OnBackButton(); + } + + if (settingsOverlay.State.Value == Visibility.Visible) { settingsOverlay.Hide(); return true; } + if (!exitConfirmed && dialogOverlay != null) + { + dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => + { + exitConfirmed = true; + this.Exit(); + })); + + return true; + } + return base.OnBackButton(); } @@ -394,5 +420,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer modSettingChangeTracker?.Dispose(); } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + if (!this.IsCurrentScreen()) + return; + + if (!client.IsHost) + { + // todo: should handle this when the request queue is implemented. + // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap + // flow may need to change to support an "unable to present" return value. + return; + } + + this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset)); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index ffcf248575..aaacf891bb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; @@ -19,8 +18,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead. - public class MultiplayerPlayer : PlaylistsPlayer + public class MultiplayerPlayer : RoomSubmittingPlayer { protected override bool PauseOnFocusLost => false; @@ -63,9 +61,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); + } - if (Token == null) - return; // Todo: Somehow handle token retrieval failure. + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + + if (!ValidForResume) + return; // token retrieval may have failed. client.MatchStarted += onMatchStarted; client.ResultsReady += onResultsReady; @@ -135,15 +138,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onResultsReady() => resultsReady.SetResult(true); - protected override async Task SubmitScore(Score score) + protected override async Task PrepareScoreForResultsAsync(Score score) { - await base.SubmitScore(score); + await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - await client.ChangeState(MultiplayerUserState.FinishedPlay); + await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false); // Await up to 60 seconds for results to become available (6 api request timeouts). // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur. - await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))); + await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false); } protected override ResultsScreen CreateResults(ScoreInfo score) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 71fd0d5c76..90e499c67f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -28,7 +28,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay { [Cached] - public abstract class OnlinePlayScreen : OsuScreen + public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack { public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; @@ -355,5 +355,7 @@ namespace osu.Game.Screens.OnlinePlay protected override double TransformDuration => 200; } } + + ScreenStack IHasSubScreenStack.SubScreenStack => screenStack; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f0c77b79bf..3f30ef1176 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -75,9 +75,18 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>(); FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>(); + Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); } + private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods) + { + FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + + // Reset the validity delegate to update the overlay's display. + freeModSelectOverlay.IsValidMod = IsValidFreeMod; + } + private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset) { FreeMods.Value = Array.Empty<Mod>(); @@ -155,6 +164,10 @@ namespace osu.Game.Screens.OnlinePlay /// </summary> /// <param name="mod">The <see cref="Mod"/> to check.</param> /// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns> - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod); + protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); + + private bool checkCompatibleFreeMod(Mod mod) + => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 2a1efbc040..5062a296a8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -362,7 +363,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Menu.MaxHeight = 100; } - protected override string GenerateItemText(TimeSpan item) => item.Humanize(); + protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize(); } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index ddc88261f7..260d4961ff 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -4,13 +4,9 @@ using System; using System.Diagnostics; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -19,36 +15,18 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.OnlinePlay.Playlists { - public class PlaylistsPlayer : Player + public class PlaylistsPlayer : RoomSubmittingPlayer { public Action Exited; - [Resolved(typeof(Room), nameof(Room.RoomID))] - protected Bindable<long?> RoomId { get; private set; } - - protected readonly PlaylistItem PlaylistItem; - - protected int? Token { get; private set; } - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private IBindable<RulesetInfo> ruleset { get; set; } - public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) - : base(configuration) + : base(playlistItem, configuration) { - PlaylistItem = playlistItem; } [BackgroundDependencyLoader] - private void load() + private void load(IBindable<RulesetInfo> ruleset) { - Token = null; - - bool failed = false; - // Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); @@ -58,29 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); - - var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash); - req.Success += r => Token = r.ID; - req.Failure += e => - { - failed = true; - - if (string.IsNullOrEmpty(e.Message)) - Logger.Error(e, "Failed to retrieve a score submission token."); - else - Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important); - - Schedule(() => - { - ValidForResume = false; - this.Exit(); - }); - }; - - api.Queue(req); - - while (!failed && !Token.HasValue) - Thread.Sleep(1000); } public override bool OnExiting(IScreen next) @@ -106,31 +61,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return score; } - protected override async Task SubmitScore(Score score) - { - await base.SubmitScore(score); - - Debug.Assert(Token != null); - - var tcs = new TaskCompletionSource<bool>(); - var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo); - - request.Success += s => - { - score.ScoreInfo.OnlineScoreID = s.ID; - tcs.SetResult(true); - }; - - request.Failure += e => - { - Logger.Error(e, "Failed to submit score"); - tcs.SetResult(false); - }; - - api.Queue(request); - await tcs.Task; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index eff06e26ee..c56344a8fb 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Play }), new OsuSpriteText { - Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.GetFont(size: 36, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play }, new OsuSpriteText { - Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = OsuFont.GetFont(size: 26, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 4784bca7dd..b4604c0d01 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Sprites; using osu.Game.Skinning; using osuTK; @@ -84,15 +83,16 @@ namespace osu.Game.Screens.Play.HUD { InternalChildren = new[] { - displayedCountSpriteText = createSpriteText().With(s => + popOutCount = new LegacySpriteText(skin, LegacyFont.Combo) { - s.Alpha = 0; - }), - popOutCount = createSpriteText().With(s => + Alpha = 0, + Margin = new MarginPadding(0.05f), + Blending = BlendingParameters.Additive, + }, + displayedCountSpriteText = new LegacySpriteText(skin, LegacyFont.Combo) { - s.Alpha = 0; - s.Margin = new MarginPadding(0.05f); - }) + Alpha = 0, + }, }; Current.ValueChanged += combo => updateCount(combo.NewValue == 0); @@ -246,7 +246,5 @@ namespace osu.Game.Screens.Play.HUD double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; return difference * rolling_duration; } - - private OsuSpriteText createSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboText)); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1e130b7f88..efe5d26409 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play { [Cached] [Cached(typeof(ISamplePlaybackDisabler))] - public class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler + public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler { /// <summary> /// The delay upon completion of the beatmap before displaying the results screen. @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play /// <summary> /// Create a new player instance. /// </summary> - public Player(PlayerConfiguration configuration = null) + protected Player(PlayerConfiguration configuration = null) { Configuration = configuration ?? new PlayerConfiguration(); } @@ -427,11 +427,18 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() { - if (!PauseOnFocusLost || breakTracker.IsBreakTime.Value) + if (!PauseOnFocusLost || !pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) - Pause(); + { + bool paused = Pause(); + + // if the initial pause could not be satisfied, the pause cooldown may be active. + // reschedule the pause attempt until it can be achieved. + if (!paused) + Scheduler.AddOnce(updatePauseOnFocusLostState); + } } private IBeatmap loadPlayableBeatmap() @@ -552,7 +559,7 @@ namespace osu.Game.Screens.Play } private ScheduledDelegate completionProgressDelegate; - private Task<ScoreInfo> scoreSubmissionTask; + private Task<ScoreInfo> prepareScoreForDisplayTask; private void updateCompletionState(ValueChangedEvent<bool> completionState) { @@ -579,22 +586,22 @@ namespace osu.Game.Screens.Play if (!Configuration.ShowResults) return; - scoreSubmissionTask ??= Task.Run(async () => + prepareScoreForDisplayTask ??= Task.Run(async () => { var score = CreateScore(); try { - await SubmitScore(score); + await PrepareScoreForResultsAsync(score).ConfigureAwait(false); } catch (Exception ex) { - Logger.Error(ex, "Score submission failed!"); + Logger.Error(ex, "Score preparation failed!"); } try { - await ImportScore(score); + await ImportScore(score).ConfigureAwait(false); } catch (Exception ex) { @@ -610,7 +617,7 @@ namespace osu.Game.Screens.Play private void scheduleCompletion() => completionProgressDelegate = Schedule(() => { - if (!scoreSubmissionTask.IsCompleted) + if (!prepareScoreForDisplayTask.IsCompleted) { scheduleCompletion(); return; @@ -618,7 +625,7 @@ namespace osu.Game.Screens.Play // screen may be in the exiting transition phase. if (this.IsCurrentScreen()) - this.Push(CreateResults(scoreSubmissionTask.Result)); + this.Push(CreateResults(prepareScoreForDisplayTask.Result)); }); protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; @@ -674,6 +681,9 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; + protected bool PauseCooldownActive => + lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; + /// <summary> /// A set of conditionals which defines whether the current game state and configuration allows for /// pausing to be attempted via <see cref="Pause"/>. If false, the game should generally exit if a user pause @@ -684,11 +694,9 @@ namespace osu.Game.Screens.Play LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value + // cannot pause if we are already in a fail state && !HasFailed; - private bool pauseCooldownActive => - lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; - private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value @@ -697,12 +705,12 @@ namespace osu.Game.Screens.Play // already resuming && !IsResuming; - public void Pause() + public bool Pause() { - if (!pausingSupportedByCurrentState) return; + if (!pausingSupportedByCurrentState) return false; - if (!IsResuming && pauseCooldownActive) - return; + if (!IsResuming && PauseCooldownActive) + return false; if (IsResuming) { @@ -713,6 +721,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Stop(); PauseOverlay.Show(); lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; + return true; } public void Resume() @@ -886,11 +895,11 @@ namespace osu.Game.Screens.Play } /// <summary> - /// Submits the player's <see cref="Score"/>. + /// Prepare the <see cref="Score"/> for display at results. /// </summary> - /// <param name="score">The <see cref="Score"/> to submit.</param> - /// <returns>The submitted score.</returns> - protected virtual Task SubmitScore(Score score) => Task.CompletedTask; + /// <param name="score">The <see cref="Score"/> to prepare.</param> + /// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns> + protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask; /// <summary> /// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>. diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 5b4bd11216..7d906cdc5b 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs new file mode 100644 index 0000000000..7ba12f5db6 --- /dev/null +++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Play +{ + /// <summary> + /// A player instance which submits to a room backing. This is generally used by playlists and multiplayer. + /// </summary> + public abstract class RoomSubmittingPlayer : SubmittingPlayer + { + [Resolved(typeof(Room), nameof(Room.RoomID))] + protected Bindable<long?> RoomId { get; private set; } + + protected readonly PlaylistItem PlaylistItem; + + protected RoomSubmittingPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) + : base(configuration) + { + PlaylistItem = playlistItem; + } + + protected override APIRequest<APIScoreToken> CreateTokenRequest() + { + if (!(RoomId.Value is long roomId)) + return null; + + return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash); + } + + protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo); + } +} diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs new file mode 100644 index 0000000000..ee1ccdc5b3 --- /dev/null +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Online.Solo; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Play +{ + public class SoloPlayer : SubmittingPlayer + { + protected override APIRequest<APIScoreToken> CreateTokenRequest() + { + if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId)) + return null; + + return new CreateSoloScoreRequest(beatmapId, Game.VersionHash); + } + + protected override bool HandleTokenRetrievalFailure(Exception exception) => false; + + protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) + { + Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null); + + int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value; + + return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo); + } + } +} diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs new file mode 100644 index 0000000000..d22199447d --- /dev/null +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -0,0 +1,141 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Play +{ + /// <summary> + /// A player instance which supports submitting scores to an online store. + /// </summary> + public abstract class SubmittingPlayer : Player + { + /// <summary> + /// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequest"/>. + /// </summary> + private long? token; + + [Resolved] + private IAPIProvider api { get; set; } + + protected SubmittingPlayer(PlayerConfiguration configuration = null) + : base(configuration) + { + } + + protected override void LoadAsyncComplete() + { + if (!handleTokenRetrieval()) return; + + base.LoadAsyncComplete(); + } + + private bool handleTokenRetrieval() + { + // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request. + var tcs = new TaskCompletionSource<bool>(); + + if (!api.IsLoggedIn) + { + handleTokenFailure(new InvalidOperationException("API is not online.")); + return false; + } + + var req = CreateTokenRequest(); + + if (req == null) + { + handleTokenFailure(new InvalidOperationException("Request could not be constructed.")); + return false; + } + + req.Success += r => + { + token = r.ID; + tcs.SetResult(true); + }; + req.Failure += handleTokenFailure; + + api.Queue(req); + + tcs.Task.Wait(); + return true; + + void handleTokenFailure(Exception exception) + { + if (HandleTokenRetrievalFailure(exception)) + { + if (string.IsNullOrEmpty(exception.Message)) + Logger.Error(exception, "Failed to retrieve a score submission token."); + else + Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important); + + Schedule(() => + { + ValidForResume = false; + this.Exit(); + }); + } + + tcs.SetResult(false); + } + } + + /// <summary> + /// Called when a token could not be retrieved for submission. + /// </summary> + /// <param name="exception">The error causing the failure.</param> + /// <returns>Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true.</returns> + protected virtual bool HandleTokenRetrievalFailure(Exception exception) => true; + + protected override async Task PrepareScoreForResultsAsync(Score score) + { + await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); + + // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). + if (token == null) + return; + + var tcs = new TaskCompletionSource<bool>(); + var request = CreateSubmissionRequest(score, token.Value); + + request.Success += s => + { + score.ScoreInfo.OnlineScoreID = s.ID; + tcs.SetResult(true); + }; + + request.Failure += e => + { + Logger.Error(e, "Failed to submit score"); + tcs.SetResult(false); + }; + + api.Queue(request); + await tcs.Task.ConfigureAwait(false); + } + + /// <summary> + /// Construct a request to be used for retrieval of the score token. + /// Can return null, at which point <see cref="HandleTokenRetrievalFailure"/> will be fired. + /// </summary> + [CanBeNull] + protected abstract APIRequest<APIScoreToken> CreateTokenRequest(); + + /// <summary> + /// Construct a request to submit the score. + /// Will only be invoked if the request constructed via <see cref="CreateTokenRequest"/> was successful. + /// </summary> + /// <param name="score">The score to be submitted.</param> + /// <param name="token">The submission token.</param> + protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token); + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index ff6203bc25..6a6b39b61c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, Truncate = true, @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, Truncate = true, diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 76b549da1a..9bc696948f 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Ranking { public class SoloResultsScreen : ResultsScreen { + private GetScoresRequest getScoreRequest; + [Resolved] private RulesetStore rulesets { get; set; } @@ -28,9 +30,16 @@ namespace osu.Game.Screens.Ranking if (Score.Beatmap.OnlineBeatmapID == null || Score.Beatmap.Status <= BeatmapSetOnlineStatus.Pending) return null; - var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); - req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); - return req; + getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); + return getScoreRequest; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + getScoreRequest?.Cancel(); } } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 86cb561bc7..4069dc82ed 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using JetBrains.Annotations; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; @@ -25,6 +24,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -38,12 +38,16 @@ namespace osu.Game.Screens.Select private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); - private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>(); + [Resolved] + private IBindable<RulesetInfo> ruleset { get; set; } + + [Resolved] + private IBindable<IReadOnlyList<Mod>> mods { get; set; } [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - private IBindable<StarDifficulty> beatmapDifficulty; + private IBindable<StarDifficulty?> beatmapDifficulty; protected BufferedWedgeInfo Info; @@ -63,11 +67,10 @@ namespace osu.Game.Screens.Select }; } - [BackgroundDependencyLoader(true)] - private void load([CanBeNull] Bindable<RulesetInfo> parentRuleset) + [BackgroundDependencyLoader] + private void load() { - ruleset.BindTo(parentRuleset); - ruleset.ValueChanged += _ => updateDisplay(); + ruleset.BindValueChanged(_ => updateDisplay()); } protected override void PopIn() @@ -132,7 +135,7 @@ namespace osu.Game.Screens.Select return; } - LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()) { Shear = -Shear, Depth = Info?.Depth + 1 ?? 0 @@ -160,20 +163,25 @@ namespace osu.Game.Screens.Select public OsuSpriteText ArtistLabel { get; private set; } public BeatmapSetOnlineStatusPill StatusPill { get; private set; } public FillFlowContainer MapperContainer { get; private set; } - public FillFlowContainer InfoLabelContainer { get; private set; } private ILocalisedBindableString titleBinding; private ILocalisedBindableString artistBinding; + private FillFlowContainer infoLabelContainer; + private Container bpmLabelContainer; private readonly WorkingBeatmap beatmap; private readonly RulesetInfo ruleset; + private readonly IReadOnlyList<Mod> mods; private readonly StarDifficulty starDifficulty; - public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, StarDifficulty difficulty) + private ModSettingChangeTracker settingChangeTracker; + + public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty) : base(pixelSnapping: true) { this.beatmap = beatmap; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; + this.mods = mods; starDifficulty = difficulty; } @@ -184,11 +192,10 @@ namespace osu.Game.Screens.Select var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); CacheDrawnFrameBuffer = true; - RelativeSizeAxes = Axes.Both; - titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title))); - artistBinding = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist))); + titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); + artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); Children = new Drawable[] { @@ -302,12 +309,11 @@ namespace osu.Game.Screens.Select AutoSizeAxes = Axes.Both, Children = getMapper(metadata) }, - InfoLabelContainer = new FillFlowContainer + infoLabelContainer = new FillFlowContainer { Margin = new MarginPadding { Top = 20 }, Spacing = new Vector2(20, 0), AutoSizeAxes = Axes.Both, - Children = getInfoLabels() } } } @@ -319,6 +325,8 @@ namespace osu.Game.Screens.Select // no difficulty means it can't have a status to show if (beatmapInfo.Version == null) StatusPill.Hide(); + + addInfoLabels(); } private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0 @@ -335,63 +343,91 @@ namespace osu.Game.Screens.Select ForceRedraw(); } - private InfoLabel[] getInfoLabels() + private void addInfoLabels() { - var b = beatmap.Beatmap; + if (beatmap.Beatmap?.HitObjects?.Any() != true) + return; - List<InfoLabel> labels = new List<InfoLabel>(); - - if (b?.HitObjects?.Any() == true) + infoLabelContainer.Children = new Drawable[] { - labels.Add(new InfoLabel(new BeatmapStatistic + new InfoLabel(new BeatmapStatistic { Name = "Length", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"), - })); - - labels.Add(new InfoLabel(new BeatmapStatistic + Content = TimeSpan.FromMilliseconds(beatmap.BeatmapInfo.Length).ToString(@"m\:ss"), + }), + bpmLabelContainer = new Container { - Name = "BPM", - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), - Content = getBPMRange(b), - })); + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(20, 0), + Children = getRulesetInfoLabels() + } + }; + + settingChangeTracker = new ModSettingChangeTracker(mods); + settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); + + refreshBPMLabel(); + } + + private InfoLabel[] getRulesetInfoLabels() + { + try + { + IBeatmap playableBeatmap; try { - IBeatmap playableBeatmap; - - try - { - // Try to get the beatmap with the user's ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>()); - } - catch (BeatmapInvalidForRulesetException) - { - // Can't be converted to the user's ruleset, so use the beatmap's own ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>()); - } - - labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); + // Try to get the beatmap with the user's ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>()); } - catch (Exception e) + catch (BeatmapInvalidForRulesetException) { - Logger.Error(e, "Could not load beatmap successfully!"); + // Can't be converted to the user's ruleset, so use the beatmap's own ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>()); } + + return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray(); + } + catch (Exception e) + { + Logger.Error(e, "Could not load beatmap successfully!"); } - return labels.ToArray(); + return Array.Empty<InfoLabel>(); } - private string getBPMRange(IBeatmap beatmap) + private void refreshBPMLabel() { - double bpmMax = beatmap.ControlPointInfo.BPMMaximum; - double bpmMin = beatmap.ControlPointInfo.BPMMinimum; + var b = beatmap.Beatmap; + if (b == null) + return; - if (Precision.AlmostEquals(bpmMin, bpmMax)) - return $"{bpmMin:0}"; + // this doesn't consider mods which apply variable rates, yet. + double rate = 1; + foreach (var mod in mods.OfType<IApplicableToRate>()) + rate = mod.ApplyToRate(0, rate); - return $"{bpmMin:0}-{bpmMax:0} (mostly {60000 / beatmap.GetMostCommonBeatLength():0})"; + double bpmMax = b.ControlPointInfo.BPMMaximum * rate; + double bpmMin = b.ControlPointInfo.BPMMinimum * rate; + double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate; + + string labelText = Precision.AlmostEquals(bpmMin, bpmMax) + ? $"{bpmMin:0}" + : $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})"; + + bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic + { + Name = "BPM", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), + Content = labelText + }); + + ForceRedraw(); } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) @@ -414,6 +450,12 @@ namespace osu.Game.Screens.Select }; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + settingChangeTracker?.Dispose(); + } + public class InfoLabel : Container, IHasTooltip { public string TooltipText { get; } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 1aab50037a..521b90202d 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel if (match) match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; + if (match && criteria.RulesetCriteria != null) + match &= criteria.RulesetCriteria.Matches(Beatmap); + Filtered.Value = !match; } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index e66469ff8d..633ef9297e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - private IBindable<StarDifficulty> starDifficultyBindable; + private IBindable<StarDifficulty?> starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) @@ -217,7 +217,10 @@ namespace osu.Game.Screens.Select.Carousel { // We've potentially cancelled the computation above so a new bindable is required. starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); - starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); + starDifficultyBindable.BindValueChanged(d => + { + starCounter.Current = (float)(d.NewValue?.Stars ?? 0); + }, true); } base.ApplyState(); diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 4e8d27f14d..23a02547b2 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -41,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)), + Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), Shadow = true, }, new OsuSpriteText { - Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)), + Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), Shadow = true, }, diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index ab4f3f4796..b0084735b1 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -15,6 +15,8 @@ using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; @@ -137,8 +139,6 @@ namespace osu.Game.Screens.Select.Details updateStarDifficulty(); } - private IBindable<StarDifficulty> normalStarDifficulty; - private IBindable<StarDifficulty> moddedStarDifficulty; private CancellationTokenSource starDifficultyCancellationSource; private void updateStarDifficulty() @@ -150,13 +150,13 @@ namespace osu.Game.Screens.Select.Details starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); - moddedStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); + var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); - normalStarDifficulty.BindValueChanged(_ => updateDisplay()); - moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); - - void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars); + Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() => + { + starDifficulty.Value = ((float)normalStarDifficulty.Result.Stars, (float)moddedStarDifficulty.Result.Stars); + }), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current); } protected override void Dispose(bool isDisposing) @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Select.Details [Resolved] private OsuColour colours { get; set; } - public string Title + public LocalisableString Title { get => name.Text; set => name.Text = value; diff --git a/osu.Game/Screens/Select/Filter/Operator.cs b/osu.Game/Screens/Select/Filter/Operator.cs new file mode 100644 index 0000000000..706daf631f --- /dev/null +++ b/osu.Game/Screens/Select/Filter/Operator.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.Select.Filter +{ + /// <summary> + /// Defines logical operators that can be used in the song select search box keyword filters. + /// </summary> + public enum Operator + { + Less, + LessOrEqual, + Equal, + GreaterOrEqual, + Greater + } +} diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index eafd8a87d1..298b6e49bd 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -36,6 +37,8 @@ namespace osu.Game.Screens.Select public FilterCriteria CreateCriteria() { + Debug.Assert(ruleset.Value.ID != null); + var query = searchTextBox.Text; var criteria = new FilterCriteria @@ -53,6 +56,8 @@ namespace osu.Game.Screens.Select if (!maximumStars.IsDefault) criteria.UserStarDifficulty.Max = maximumStars.Value; + criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria(); + FilterQueryParser.ApplyQueries(criteria, query); return criteria; } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 7bddb3e51b..208048380a 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Rulesets; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select @@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select [CanBeNull] public BeatmapCollection Collection; + [CanBeNull] + public IRulesetFilterCriteria RulesetCriteria { get; set; } + public struct OptionalRange<T> : IEquatable<OptionalRange<T>> where T : struct { diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 4b6b3be45c..ea7f233bea 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -5,13 +5,17 @@ using System; using System.Globalization; using System.Text.RegularExpressions; using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select { - internal static class FilterQueryParser + /// <summary> + /// Utility class used for parsing song select filter queries entered via the search box. + /// </summary> + public static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?<key>stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))", + @"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -19,62 +23,81 @@ namespace osu.Game.Screens.Select foreach (Match match in query_syntax_regex.Matches(query)) { var key = match.Groups["key"].Value.ToLower(); - var op = match.Groups["op"].Value; + var op = parseOperator(match.Groups["op"].Value); var value = match.Groups["value"].Value; - parseKeywordCriteria(criteria, key, value, op); - - query = query.Replace(match.ToString(), ""); + if (tryParseKeywordCriteria(criteria, key, value, op)) + query = query.Replace(match.ToString(), ""); } criteria.SearchText = query; } - private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op) + private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op) { switch (key) { - case "stars" when parseFloatWithPoint(value, out var stars): - updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2); - break; + case "stars": + return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); - case "ar" when parseFloatWithPoint(value, out var ar): - updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2); - break; + case "ar": + return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value); - case "dr" when parseFloatWithPoint(value, out var dr): - case "hp" when parseFloatWithPoint(value, out dr): - updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); - break; + case "dr": + case "hp": + return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value); - case "cs" when parseFloatWithPoint(value, out var cs): - updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2); - break; + case "cs": + return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value); - case "bpm" when parseDoubleWithPoint(value, out var bpm): - updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2); - break; + case "bpm": + return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2); - case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length): - var scale = getLengthScale(value); - updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0); - break; + case "length": + return tryUpdateLengthRange(criteria, op, value); - case "divisor" when parseInt(value, out var divisor): - updateCriteriaRange(ref criteria.BeatDivisor, op, divisor); - break; + case "divisor": + return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); - case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue): - updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue); - break; + case "status": + return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, + (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val)); case "creator": - updateCriteriaText(ref criteria.Creator, op, value); - break; + return TryUpdateCriteriaText(ref criteria.Creator, op, value); case "artist": - updateCriteriaText(ref criteria.Artist, op, value); - break; + return TryUpdateCriteriaText(ref criteria.Artist, op, value); + + default: + return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false; + } + } + + private static Operator parseOperator(string value) + { + switch (value) + { + case "=": + case ":": + return Operator.Equal; + + case "<": + return Operator.Less; + + case "<=": + case "<:": + return Operator.LessOrEqual; + + case ">": + return Operator.Greater; + + case ">=": + case ">:": + return Operator.GreaterOrEqual; + + default: + throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}"); } } @@ -84,129 +107,203 @@ namespace osu.Game.Screens.Select value.EndsWith('m') ? 60000 : value.EndsWith('h') ? 3600000 : 1000; - private static bool parseFloatWithPoint(string value, out float result) => + private static bool tryParseFloatWithPoint(string value, out float result) => float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); - private static bool parseDoubleWithPoint(string value, out double result) => + private static bool tryParseDoubleWithPoint(string value, out double result) => double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); - private static bool parseInt(string value, out int result) => + private static bool tryParseInt(string value, out int result) => int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); - private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value) + /// <summary> + /// Attempts to parse a keyword filter with the specified <paramref name="op"/> and textual <paramref name="value"/>. + /// If the value indicates a valid textual filter, the function returns <c>true</c> and the resulting data is stored into + /// <paramref name="textFilter"/>. + /// </summary> + /// <param name="textFilter">The <see cref="FilterCriteria.OptionalTextFilter"/> to store the parsed data into, if successful.</param> + /// <param name="op"> + /// The operator for the keyword filter. + /// Only <see cref="Operator.Equal"/> is valid for textual filters. + /// </param> + /// <param name="value">The value of the keyword filter.</param> + public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value) { switch (op) { - case "=": - case ":": + case Operator.Equal: textFilter.SearchTerm = value.Trim('"'); - break; + return true; + + default: + return false; } } - private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f) + /// <summary> + /// Attempts to parse a keyword filter of type <see cref="float"/> + /// from the specified <paramref name="op"/> and <paramref name="val"/>. + /// If <paramref name="val"/> can be parsed as a <see cref="float"/>, the function returns <c>true</c> + /// and the resulting range constraint is stored into <paramref name="range"/>. + /// </summary> + /// <param name="range"> + /// The <see cref="float"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/> + /// to store the parsed data into, if successful. + /// </param> + /// <param name="op">The operator for the keyword filter.</param> + /// <param name="val">The value of the keyword filter.</param> + /// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param> + public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f) + => tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance); + + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f) { switch (op) { default: - return; + return false; - case "=": - case ":": + case Operator.Equal: range.Min = value - tolerance; range.Max = value + tolerance; break; - case ">": + case Operator.Greater: range.Min = value + tolerance; break; - case ">=": - case ">:": + case Operator.GreaterOrEqual: range.Min = value - tolerance; break; - case "<": + case Operator.Less: range.Max = value - tolerance; break; - case "<=": - case "<:": + case Operator.LessOrEqual: range.Max = value + tolerance; break; } + + return true; } - private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05) + /// <summary> + /// Attempts to parse a keyword filter of type <see cref="double"/> + /// from the specified <paramref name="op"/> and <paramref name="val"/>. + /// If <paramref name="val"/> can be parsed as a <see cref="double"/>, the function returns <c>true</c> + /// and the resulting range constraint is stored into <paramref name="range"/>. + /// </summary> + /// <param name="range"> + /// The <see cref="double"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/> + /// to store the parsed data into, if successful. + /// </param> + /// <param name="op">The operator for the keyword filter.</param> + /// <param name="val">The value of the keyword filter.</param> + /// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param> + public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05) + => tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance); + + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05) { switch (op) { default: - return; + return false; - case "=": - case ":": + case Operator.Equal: range.Min = value - tolerance; range.Max = value + tolerance; break; - case ">": + case Operator.Greater: range.Min = value + tolerance; break; - case ">=": - case ">:": + case Operator.GreaterOrEqual: range.Min = value - tolerance; break; - case "<": + case Operator.Less: range.Max = value - tolerance; break; - case "<=": - case "<:": + case Operator.LessOrEqual: range.Max = value + tolerance; break; } + + return true; } - private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value) + /// <summary> + /// Used to determine whether the string value <paramref name="val"/> can be converted to type <typeparamref name="T"/>. + /// If conversion can be performed, the delegate returns <c>true</c> + /// and the conversion result is returned in the <c>out</c> parameter <paramref name="parsed"/>. + /// </summary> + /// <param name="val">The string value to attempt parsing for.</param> + /// <param name="parsed">The parsed value, if conversion is possible.</param> + public delegate bool TryParseFunction<T>(string val, out T parsed); + + /// <summary> + /// Attempts to parse a keyword filter of type <typeparamref name="T"/>, + /// from the specified <paramref name="op"/> and <paramref name="val"/>. + /// If <paramref name="val"/> can be parsed into <typeparamref name="T"/> using <paramref name="parseFunction"/>, the function returns <c>true</c> + /// and the resulting range constraint is stored into <paramref name="range"/>. + /// </summary> + /// <param name="range">The <see cref="FilterCriteria.OptionalRange{T}"/> to store the parsed data into, if successful.</param> + /// <param name="op">The operator for the keyword filter.</param> + /// <param name="val">The value of the keyword filter.</param> + /// <param name="parseFunction">Function used to determine if <paramref name="val"/> can be converted to type <typeparamref name="T"/>.</param> + public static bool TryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string val, TryParseFunction<T> parseFunction) + where T : struct + => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted); + + private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value) where T : struct { switch (op) { default: - return; + return false; - case "=": - case ":": + case Operator.Equal: range.IsLowerInclusive = range.IsUpperInclusive = true; range.Min = value; range.Max = value; break; - case ">": + case Operator.Greater: range.IsLowerInclusive = false; range.Min = value; break; - case ">=": - case ">:": + case Operator.GreaterOrEqual: range.IsLowerInclusive = true; range.Min = value; break; - case "<": + case Operator.Less: range.IsUpperInclusive = false; range.Max = value; break; - case "<=": - case "<:": + case Operator.LessOrEqual: range.IsUpperInclusive = true; range.Max = value; break; } + + return true; + } + + private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val) + { + if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length)) + return false; + + var scale = getLengthScale(val); + return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0); } } } diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 35970cd960..cd7c1c449f 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; @@ -21,9 +22,9 @@ namespace osu.Game.Screens.Select protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); - public string Text + public LocalisableString Text { - get => SpriteText?.Text; + get => SpriteText?.Text ?? default; set { if (SpriteText != null) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 6e2f3cc9df..845c0a914e 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -39,13 +40,13 @@ namespace osu.Game.Screens.Select.Options set => iconText.Icon = value; } - public string FirstLineText + public LocalisableString FirstLineText { get => firstLine.Text; set => firstLine.Text = value; } - public string SecondLineText + public LocalisableString SecondLineText { get => secondLine.Text; set => secondLine.Text = value; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e61d5cce85..dfb4b59060 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Select SampleConfirm?.Play(); - this.Push(player = new PlayerLoader(() => new Player())); + this.Push(player = new PlayerLoader(() => new SoloPlayer())); return true; } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 346c7b3c65..0b3f5f3cde 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -24,7 +24,7 @@ namespace osu.Game.Skinning public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; - public override Sample GetSample(ISampleInfo sampleInfo) => null; + public override ISample GetSample(ISampleInfo sampleInfo) => null; public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) { diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index b01be2d5a0..a345e060e5 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -9,7 +9,5 @@ namespace osu.Game.Skinning ScoreCounter, AccuracyCounter, HealthDisplay, - ScoreText, - ComboText, } } diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index ef8de01042..73f7cf6d39 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -48,7 +48,7 @@ namespace osu.Game.Skinning /// <param name="sampleInfo">The requested sample.</param> /// <returns>A matching sample channel, or null if unavailable.</returns> [CanBeNull] - Sample GetSample(ISampleInfo sampleInfo); + ISample GetSample(ISampleInfo sampleInfo); /// <summary> /// Retrieve a configuration value. diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 5eda374337..7d6f1dc916 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -29,9 +29,11 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() - => (OsuSpriteText)skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) - ?.With(s => s.Anchor = s.Origin = Anchor.TopRight); + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; protected override void Update() { diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index fb4207b647..3ec205e897 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning return base.GetConfig<TLookup, TValue>(lookup); } - public override Sample GetSample(ISampleInfo sampleInfo) + public override ISample GetSample(ISampleInfo sampleInfo) { if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) { diff --git a/osu.Game/Skinning/LegacyFont.cs b/osu.Game/Skinning/LegacyFont.cs new file mode 100644 index 0000000000..d1971cb84c --- /dev/null +++ b/osu.Game/Skinning/LegacyFont.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + /// <summary> + /// The type of legacy font to use for <see cref="LegacySpriteText"/>s. + /// </summary> + public enum LegacyFont + { + Score, + Combo, + HitCircle, + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 0a1de461ea..5308640bdd 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -31,8 +31,6 @@ namespace osu.Game.Skinning protected override void ParseLine(List<LegacyManiaSkinConfiguration> output, Section section, string line) { - line = StripComments(line); - switch (section) { case Section.Mania: diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs index 8aa9d4e9af..0261db0e64 100644 --- a/osu.Game/Skinning/LegacyRollingCounter.cs +++ b/osu.Game/Skinning/LegacyRollingCounter.cs @@ -4,7 +4,6 @@ using System; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Skinning { @@ -14,9 +13,7 @@ namespace osu.Game.Skinning public class LegacyRollingCounter : RollingCounter<int> { private readonly ISkin skin; - - private readonly string fontName; - private readonly float fontOverlap; + private readonly LegacyFont font; protected override bool IsRollingProportional => true; @@ -24,17 +21,11 @@ namespace osu.Game.Skinning /// Creates a new <see cref="LegacyRollingCounter"/>. /// </summary> /// <param name="skin">The <see cref="ISkin"/> from which to get counter number sprites.</param> - /// <param name="fontName">The name of the legacy font to use.</param> - /// <param name="fontOverlap"> - /// The numeric overlap of number sprites to use. - /// A positive number will bring the number sprites closer together, while a negative number - /// will split them apart more. - /// </param> - public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) + /// <param name="font">The legacy font to use for the counter.</param> + public LegacyRollingCounter(ISkin skin, LegacyFont font) { this.skin = skin; - this.fontName = fontName; - this.fontOverlap = fontOverlap; + this.font = font; } protected override double GetProportionalDuration(int currentValue, int newValue) @@ -42,10 +33,6 @@ namespace osu.Game.Skinning return Math.Abs(newValue - currentValue) * 75.0; } - protected sealed override OsuSpriteText CreateSpriteText() => - new LegacySpriteText(skin, fontName) - { - Spacing = new Vector2(-fontOverlap, 0f) - }; + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, font); } } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 5bffeff5a8..1d330ef495 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -33,8 +33,10 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - protected sealed override OsuSpriteText CreateSpriteText() - => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) - .With(s => s.Anchor = s.Origin = Anchor.TopRight); + protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e5d0217671..eae3b69233 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -18,7 +18,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; -using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -100,13 +99,6 @@ namespace osu.Game.Skinning true) != null); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - Textures?.Dispose(); - Samples?.Dispose(); - } - public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) { switch (lookup) @@ -327,19 +319,13 @@ namespace osu.Game.Skinning return null; } - private string scorePrefix => GetConfig<LegacySkinConfiguration.LegacySetting, string>(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; - - private string comboPrefix => GetConfig<LegacySkinConfiguration.LegacySetting, string>(LegacySkinConfiguration.LegacySetting.ComboPrefix)?.Value ?? "score"; - - private bool hasScoreFont => this.HasFont(scorePrefix); - public override Drawable GetDrawableComponent(ISkinComponent component) { switch (component) { case HUDSkinComponent hudComponent: { - if (!hasScoreFont) + if (!this.HasFont(LegacyFont.Score)) return null; switch (hudComponent.Component) @@ -355,18 +341,6 @@ namespace osu.Game.Skinning case HUDSkinComponents.HealthDisplay: return new LegacyHealthDisplay(this); - - case HUDSkinComponents.ComboText: - return new LegacySpriteText(this, comboPrefix) - { - Spacing = new Vector2(-(GetConfig<LegacySkinConfiguration.LegacySetting, int>(LegacySkinConfiguration.LegacySetting.ComboOverlap)?.Value ?? -2), 0) - }; - - case HUDSkinComponents.ScoreText: - return new LegacySpriteText(this, scorePrefix) - { - Spacing = new Vector2(-(GetConfig<LegacySkinConfiguration.LegacySetting, int>(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2), 0) - }; } return null; @@ -452,7 +426,7 @@ namespace osu.Game.Skinning return null; } - public override Sample GetSample(ISampleInfo sampleInfo) + public override ISample GetSample(ISampleInfo sampleInfo) { IEnumerable<string> lookupNames; @@ -504,5 +478,12 @@ namespace osu.Game.Skinning string lastPiece = componentName.Split('/').Last(); yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + Textures?.Dispose(); + Samples?.Dispose(); + } } } diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 84a834ec22..20d1da8aaa 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -19,6 +19,8 @@ namespace osu.Game.Skinning ComboOverlap, ScorePrefix, ScoreOverlap, + HitCirclePrefix, + HitCircleOverlap, AnimationFramerate, LayeredHitSounds } diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs index 75b7ba28b9..2700f84815 100644 --- a/osu.Game/Skinning/LegacySkinDecoder.cs +++ b/osu.Game/Skinning/LegacySkinDecoder.cs @@ -17,8 +17,6 @@ namespace osu.Game.Skinning { if (section != Section.Colours) { - line = StripComments(line); - var pair = SplitKeyVal(line); switch (section) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index a7c084998d..d8fb1fa664 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -63,8 +64,51 @@ namespace osu.Game.Skinning } } - public static bool HasFont(this ISkin source, string fontPrefix) - => source.GetTexture($"{fontPrefix}-0") != null; + public static bool HasFont(this ISkin source, LegacyFont font) + { + return source.GetTexture($"{source.GetFontPrefix(font)}-0") != null; + } + + public static string GetFontPrefix(this ISkin source, LegacyFont font) + { + switch (font) + { + case LegacyFont.Score: + return source.GetConfig<LegacySetting, string>(LegacySetting.ScorePrefix)?.Value ?? "score"; + + case LegacyFont.Combo: + return source.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score"; + + case LegacyFont.HitCircle: + return source.GetConfig<LegacySetting, string>(LegacySetting.HitCirclePrefix)?.Value ?? "default"; + + default: + throw new ArgumentOutOfRangeException(nameof(font)); + } + } + + /// <summary> + /// Returns the numeric overlap of number sprites to use. + /// A positive number will bring the number sprites closer together, while a negative number + /// will split them apart more. + /// </summary> + public static float GetFontOverlap(this ISkin source, LegacyFont font) + { + switch (font) + { + case LegacyFont.Score: + return source.GetConfig<LegacySetting, float>(LegacySetting.ScoreOverlap)?.Value ?? 0f; + + case LegacyFont.Combo: + return source.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? 0f; + + case LegacyFont.HitCircle: + return source.GetConfig<LegacySetting, float>(LegacySetting.HitCircleOverlap)?.Value ?? -2f; + + default: + throw new ArgumentOutOfRangeException(nameof(font)); + } + } public class SkinnableTextureAnimation : TextureAnimation { diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index e2f4a82a54..ae8faf1a3b 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Source.GetTexture(componentName, wrapModeS, wrapModeT); - public virtual Sample GetSample(ISampleInfo sampleInfo) + public virtual ISample GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) return Source.GetSample(sampleInfo); diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 5d0e312f7c..c55400e219 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; using osu.Framework.Text; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Skinning { @@ -16,12 +17,14 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; - public LegacySpriteText(ISkin skin, string font = "score") + public LegacySpriteText(ISkin skin, LegacyFont font) { Shadow = false; UseFullGlyphHeight = false; - Font = new FontUsage(font, 1, fixedWidth: true); + Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); + Spacing = new Vector2(-skin.GetFontOverlap(font), 0); + glyphStore = new LegacyGlyphStore(skin); } diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index abff57091b..b04158a58f 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -17,7 +17,7 @@ namespace osu.Game.Skinning /// <summary> /// A sample corresponding to an <see cref="ISampleInfo"/> that supports being pooled and responding to skin changes. /// </summary> - public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + public class PoolableSkinnableSample : SkinReloadableDrawable, IAdjustableAudioComponent { /// <summary> /// The currently-loaded <see cref="DrawableSample"/>. @@ -86,21 +86,21 @@ namespace osu.Game.Skinning sampleContainer.Clear(); Sample = null; - var ch = CurrentSkin.GetSample(sampleInfo); + var sample = CurrentSkin.GetSample(sampleInfo); - if (ch == null && AllowDefaultFallback) + if (sample == null && AllowDefaultFallback) { foreach (var lookup in sampleInfo.LookupNames) { - if ((ch = sampleStore.Get(lookup)) != null) + if ((sample = sampleStore.Get(lookup)) != null) break; } } - if (ch == null) + if (sample == null) return; - sampleContainer.Add(Sample = new DrawableSample(ch)); + sampleContainer.Add(Sample = new DrawableSample(sample)); // Start playback internally for the new sample if the previous one was playing beforehand. if (wasPlaying && Looping) @@ -165,6 +165,10 @@ namespace osu.Game.Skinning public BindableNumber<double> Tempo => sampleContainer.Tempo; + public void BindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.BindAdjustments(component); + + public void UnbindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.UnbindAdjustments(component); + public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e8d84b49f9..6d1bce2cb1 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -19,7 +19,7 @@ namespace osu.Game.Skinning public abstract Drawable GetDrawableComponent(ISkinComponent componentName); - public abstract Sample GetSample(ISampleInfo sampleInfo); + public abstract ISample GetSample(ISampleInfo sampleInfo); public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); @@ -36,6 +36,7 @@ namespace osu.Game.Skinning ~Skin() { + // required to potentially clean up sample store from audio hierarchy. Dispose(false); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2826c826a5..ac4d63159a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -86,7 +86,7 @@ namespace osu.Game.Skinning public void SelectRandomSkin() { // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = GetAllUsableSkins().Where(s => s.ID > 0 && s.ID != CurrentSkinInfo.Value.ID).ToArray(); + var randomChoices = GetAllUsableSkins().Where(s => s.ID != CurrentSkinInfo.Value.ID).ToArray(); if (randomChoices.Length == 0) { @@ -104,7 +104,7 @@ namespace osu.Game.Skinning protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) { // we need to populate early to create a hash based off skin.ini contents - if (item.Name?.Contains(".osk") == true) + if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) populateMetadata(item); if (item.Creator != null && item.Creator != unknown_creator_string) @@ -120,9 +120,9 @@ namespace osu.Game.Skinning protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { - await base.Populate(model, archive, cancellationToken); + await base.Populate(model, archive, cancellationToken).ConfigureAwait(false); - if (model.Name?.Contains(".osk") == true) + if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) populateMetadata(model); } @@ -137,7 +137,7 @@ namespace osu.Game.Skinning } else { - item.Name = item.Name.Replace(".osk", ""); + item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase); item.Creator ??= unknown_creator_string; } } @@ -171,7 +171,7 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT); - public Sample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); + public ISample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => CurrentSkin.Value.GetConfig<TLookup, TValue>(lookup); diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index ba67d0a678..cf22b2e820 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -59,9 +59,9 @@ namespace osu.Game.Skinning return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT); } - public Sample GetSample(ISampleInfo sampleInfo) + public ISample GetSample(ISampleInfo sampleInfo) { - Sample sourceChannel; + ISample sourceChannel; if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null) return sourceChannel; diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index d3dfcb1dc0..f935adf7a5 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -131,6 +131,15 @@ namespace osu.Game.Skinning }); } + protected override void LoadAsyncComplete() + { + // ensure samples are constructed before SkinChanged() is called via base.LoadAsyncComplete(). + if (!samplesContainer.Any()) + updateSamples(); + + base.LoadAsyncComplete(); + } + /// <summary> /// Stops the samples. /// </summary> @@ -139,12 +148,6 @@ namespace osu.Game.Skinning samplesContainer.ForEach(c => c.Stop()); } - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - updateSamples(); - } - private void updateSamples() { bool wasPlaying = IsPlaying; @@ -176,14 +179,15 @@ namespace osu.Game.Skinning public BindableNumber<double> Tempo => samplesContainer.Tempo; - public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) - => samplesContainer.AddAdjustment(type, adjustBindable); + public void BindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.BindAdjustments(component); - public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) - => samplesContainer.RemoveAdjustment(type, adjustBindable); + public void UnbindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.UnbindAdjustments(component); - public void RemoveAllAdjustments(AdjustableProperty type) - => samplesContainer.RemoveAllAdjustments(type); + public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => samplesContainer.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => samplesContainer.RemoveAllAdjustments(type); /// <summary> /// Whether any samples are currently playing. @@ -192,6 +196,14 @@ namespace osu.Game.Skinning public bool IsPlayed => samplesContainer.Any(s => s.Played); + public IBindable<double> AggregateVolume => samplesContainer.AggregateVolume; + + public IBindable<double> AggregateBalance => samplesContainer.AggregateBalance; + + public IBindable<double> AggregateFrequency => samplesContainer.AggregateFrequency; + + public IBindable<double> AggregateTempo => samplesContainer.AggregateTempo; + #endregion } } diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs index 567dd348e1..06461127b1 100644 --- a/osu.Game/Skinning/SkinnableSpriteText.cs +++ b/osu.Game/Skinning/SkinnableSpriteText.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Skinning { @@ -21,9 +22,9 @@ namespace osu.Game.Skinning textDrawable.Text = Text; } - private string text; + private LocalisableString text; - public string Text + public LocalisableString Text { get => text; set diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 6ce3b617e9..c478b91c22 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -45,11 +45,30 @@ namespace osu.Game.Storyboards }; } + /// <summary> + /// Returns the earliest visible time. Will be null unless this group's first <see cref="Alpha"/> command has a start value of zero. + /// </summary> + public double? EarliestDisplayedTime + { + get + { + var first = Alpha.Commands.FirstOrDefault(); + + return first?.StartValue == 0 ? first.StartTime : (double?)null; + } + } + [JsonIgnore] public double CommandsStartTime { get { + // if the first alpha command starts at zero it should be given priority over anything else. + // this is due to it creating a state where the target is not present before that time, causing any other events to not be visible. + var earliestDisplay = EarliestDisplayedTime; + if (earliestDisplay != null) + return earliestDisplay.Value; + double min = double.MaxValue; for (int i = 0; i < timelines.Length; i++) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 7eac994e07..81623a9307 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; @@ -80,17 +81,17 @@ namespace osu.Game.Storyboards.Drawables if (FlipH) { - if (origin.HasFlag(Anchor.x0)) + if (origin.HasFlagFast(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlag(Anchor.x2)) + else if (origin.HasFlagFast(Anchor.x2)) origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } if (FlipV) { - if (origin.HasFlag(Anchor.y0)) + if (origin.HasFlagFast(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlag(Anchor.y2)) + else if (origin.HasFlagFast(Anchor.y2)) origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 7b1a6d54da..eb877f3dff 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -80,17 +81,17 @@ namespace osu.Game.Storyboards.Drawables if (FlipH) { - if (origin.HasFlag(Anchor.x0)) + if (origin.HasFlagFast(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlag(Anchor.x2)) + else if (origin.HasFlagFast(Anchor.x2)) origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } if (FlipV) { - if (origin.HasFlag(Anchor.y0)) + if (origin.HasFlagFast(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlag(Anchor.y2)) + else if (origin.HasFlagFast(Anchor.y2)) origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index f411ad04f3..fdaa59d7d9 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -24,13 +24,46 @@ namespace osu.Game.Storyboards public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); - public double StartTime => Math.Min( - TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue, - loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue); + public double StartTime + { + get + { + // check for presence affecting commands as an initial pass. + double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue; - public double EndTime => Math.Max( - TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue, - loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue); + foreach (var l in loops) + { + if (!(l.EarliestDisplayedTime is double lEarliest)) + continue; + + earliestStartTime = Math.Min(earliestStartTime, lEarliest); + } + + if (earliestStartTime < double.MaxValue) + return earliestStartTime; + + // if an alpha-affecting command was not found, use the earliest of any command. + earliestStartTime = TimelineGroup.StartTime; + + foreach (var l in loops) + earliestStartTime = Math.Min(earliestStartTime, l.StartTime); + + return earliestStartTime; + } + } + + public double EndTime + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in loops) + latestEndTime = Math.Max(latestEndTime, l.EndTime); + + return latestEndTime; + } + } public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 2e8c834c65..7775c2bd24 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -7,8 +7,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { @@ -48,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomManager.Schedule(() => RoomManager.PartRoom()); if (joinRoom) + { + Room.Name.Value = "test name"; + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }); + RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); + } }); public override void SetUpSteps() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 379bb758c5..09fcc1ff47 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -3,12 +3,15 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -25,6 +28,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private readonly TestMultiplayerRoomManager roomManager; + + public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) + { + this.roomManager = roomManager; + } + public void Connect() => isConnected.Value = true; public void Disconnect() => isConnected.Value = false; @@ -89,13 +102,28 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task<MultiplayerRoom> JoinRoom(long roomId) { - var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; + var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); - var room = new MultiplayerRoom(roomId); - room.Users.Add(user); + var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) + { + User = api.LocalUser.Value + }; - if (room.Users.Count == 1) - room.Host = user; + var room = new MultiplayerRoom(roomId) + { + Settings = + { + Name = apiRoom.Name.Value, + BeatmapID = apiRoom.Playlist.Last().BeatmapID, + RulesetID = apiRoom.Playlist.Last().RulesetID, + BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash, + RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(), + AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(), + PlaylistItemId = apiRoom.Playlist.Last().ID + }, + Users = { user }, + Host = user + }; return Task.FromResult(room); } @@ -108,7 +136,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); - await ((IMultiplayerClient)this).SettingsChanged(settings); + await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false); foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.Idle); @@ -150,5 +178,19 @@ namespace osu.Game.Tests.Visual.Multiplayer return ((IMultiplayerClient)this).LoadRequested(); } + + protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + { + Debug.Assert(Room != null); + + var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID); + var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet + ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet; + + if (set == null) + throw new InvalidOperationException("Beatmap not found."); + + return Task.FromResult(set); + } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs index 860caef071..e57411d04d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs @@ -32,11 +32,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { RelativeSizeAxes = Axes.Both; + RoomManager = new TestMultiplayerRoomManager(); + Client = new TestMultiplayerClient(RoomManager); + OngoingOperationTracker = new OngoingOperationTracker(); + AddRangeInternal(new Drawable[] { - Client = new TestMultiplayerClient(), - RoomManager = new TestMultiplayerRoomManager(), - OngoingOperationTracker = new OngoingOperationTracker(), + Client, + RoomManager, + OngoingOperationTracker, content = new Container { RelativeSizeAxes = Axes.Both } }); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 5e12156f3c..315be510a3 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria()); - private readonly List<Room> rooms = new List<Room>(); + public new readonly List<Room> Rooms = new List<Room>(); protected override void LoadComplete() { @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer int currentScoreId = 0; int currentRoomId = 0; + int currentPlaylistItemId = 0; ((DummyAPIAccess)api).HandleRequest = req => { @@ -46,22 +47,25 @@ namespace osu.Game.Tests.Visual.Multiplayer createdRoom.CopyFrom(createRoomRequest.Room); createdRoom.RoomID.Value ??= currentRoomId++; - rooms.Add(createdRoom); + for (int i = 0; i < createdRoom.Playlist.Count; i++) + createdRoom.Playlist[i].ID = currentPlaylistItemId++; + + Rooms.Add(createdRoom); createRoomRequest.TriggerSuccess(createdRoom); - break; + return true; case JoinRoomRequest joinRoomRequest: joinRoomRequest.TriggerSuccess(); - break; + return true; case PartRoomRequest partRoomRequest: partRoomRequest.TriggerSuccess(); - break; + return true; case GetRoomsRequest getRoomsRequest: var roomsWithoutParticipants = new List<Room>(); - foreach (var r in rooms) + foreach (var r in Rooms) { var newRoom = new Room(); @@ -72,11 +76,11 @@ namespace osu.Game.Tests.Visual.Multiplayer } getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); - break; + return true; case GetRoomRequest getRoomRequest: - getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); - break; + getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); + return true; case GetBeatmapSetRequest getBeatmapSetRequest: var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type); @@ -85,11 +89,11 @@ namespace osu.Game.Tests.Visual.Multiplayer // Get the online API from the game's dependencies. game.Dependencies.Get<IAPIProvider>().Queue(onlineReq); - break; + return true; case CreateRoomScoreRequest createRoomScoreRequest: createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); - break; + return true; case SubmitRoomScoreRequest submitRoomScoreRequest: submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore @@ -104,8 +108,10 @@ namespace osu.Game.Tests.Visual.Multiplayer User = api.LocalUser.Value, Statistics = new Dictionary<HitResult, int>() }); - break; + return true; } + + return false; }; } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index f47391ce6a..0addc9de75 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual public new HealthProcessor HealthProcessor => base.HealthProcessor; + public new bool PauseCooldownActive => base.PauseCooldownActive; + public readonly List<JudgementResult> Results = new List<JudgementResult>(); public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 4ebf2a7368..50572a7867 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -37,7 +37,7 @@ namespace osu.Game.Updater { var releases = new OsuJsonWebRequest<GitHubRelease>("https://api.github.com/repos/ppy/osu/releases/latest"); - await releases.PerformAsync(); + await releases.PerformAsync().ConfigureAwait(false); var latest = releases.ResponseObject; @@ -77,7 +77,7 @@ namespace osu.Game.Updater bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal)); break; - case RuntimeInfo.Platform.MacOsx: + case RuntimeInfo.Platform.macOS: bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal)); break; diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index f772c6d282..1c72f3ebe2 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -52,7 +52,7 @@ namespace osu.Game.Updater // debug / local compilations will reset to a non-release string. // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). - config.Set(OsuSetting.Version, version); + config.SetValue(OsuSetting.Version, version); } private readonly object updateTaskLock = new object(); @@ -69,7 +69,7 @@ namespace osu.Game.Updater lock (updateTaskLock) waitTask = (updateCheckTask ??= PerformUpdateCheck()); - bool hasUpdates = await waitTask; + bool hasUpdates = await waitTask.ConfigureAwait(false); lock (updateTaskLock) updateCheckTask = null; diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 4a6fd540c7..74ffb7c457 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -72,9 +72,6 @@ namespace osu.Game.Users [JsonProperty(@"support_level")] public int SupportLevel; - [JsonProperty(@"current_mode_rank")] - public int? CurrentModeRank; - [JsonProperty(@"is_gmt")] public bool IsGMT; @@ -114,9 +111,6 @@ namespace osu.Game.Users [JsonProperty(@"twitter")] public string Twitter; - [JsonProperty(@"skype")] - public string Skype; - [JsonProperty(@"discord")] public string Discord; @@ -126,6 +120,9 @@ namespace osu.Game.Users [JsonProperty(@"post_count")] public int PostCount; + [JsonProperty(@"comments_count")] + public int CommentsCount; + [JsonProperty(@"follower_count")] public int FollowerCount; @@ -182,7 +179,7 @@ namespace osu.Game.Users private UserStatistics statistics; /// <summary> - /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> response). + /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response). /// Otherwise empty. /// </summary> [JsonProperty(@"statistics")] diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 78e6f5a05a..04a358436e 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -29,16 +29,9 @@ namespace osu.Game.Users [JsonProperty(@"global_rank")] public int? GlobalRank; + [JsonProperty(@"country_rank")] public int? CountryRank; - [JsonProperty(@"rank")] - private UserRanks ranks - { - // eventually that will also become an own json property instead of reading from a `rank` object. - // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. - set => CountryRank = value.Country; - } - // populated via User model, as that's where the data currently lives. public RankHistoryData RankHistory; @@ -49,7 +42,7 @@ namespace osu.Game.Users public long RankedScore; [JsonProperty(@"hit_accuracy")] - public decimal Accuracy; + public double Accuracy; [JsonIgnore] public string DisplayAccuracy => Accuracy.FormatAccuracy(); @@ -119,13 +112,5 @@ namespace osu.Game.Users } } } - -#pragma warning disable 649 - private struct UserRanks - { - [JsonProperty(@"country")] - public int? Country; - } -#pragma warning restore 649 } } diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index 2578d8d835..df1b6cf00d 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Globalization; using Humanizer; namespace osu.Game.Utils @@ -10,16 +12,19 @@ namespace osu.Game.Utils /// <summary> /// Turns the provided accuracy into a percentage with 2 decimal places. /// </summary> - /// <param name="accuracy">The accuracy to be formatted</param> + /// <param name="accuracy">The accuracy to be formatted.</param> + /// <param name="formatProvider">An optional format provider.</param> /// <returns>formatted accuracy in percentage</returns> - public static string FormatAccuracy(this double accuracy) => $"{accuracy:0.00%}"; + public static string FormatAccuracy(this double accuracy, IFormatProvider formatProvider = null) + { + // for the sake of display purposes, we don't want to show a user a "rounded up" percentage to the next whole number. + // ie. a score which gets 89.99999% shouldn't ever show as 90%. + // the reasoning for this is that cutoffs for grade increases are at whole numbers and displaying the required + // percentile with a non-matching grade is confusing. + accuracy = Math.Floor(accuracy * 10000) / 10000; - /// <summary> - /// Turns the provided accuracy into a percentage with 2 decimal places. - /// </summary> - /// <param name="accuracy">The accuracy to be formatted</param> - /// <returns>formatted accuracy in percentage</returns> - public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%"; + return accuracy.ToString("0.00%", formatProvider ?? CultureInfo.CurrentCulture); + } /// <summary> /// Formats the supplied rank/leaderboard position in a consistent, simplified way. diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index be9d01cde6..8f12760a6b 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -86,11 +86,6 @@ namespace osu.Game.Utils #region Disposal - ~SentryLogger() - { - Dispose(false); - } - public void Dispose() { Dispose(true); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9c3d0c2020..6d571218fc 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,21 +18,21 @@ </None> </ItemGroup> <ItemGroup Label="Package References"> - <PackageReference Include="DiffPlex" Version="1.6.3" /> + <PackageReference Include="DiffPlex" Version="1.7.0" /> <PackageReference Include="Humanizer" Version="2.8.26" /> <PackageReference Include="MessagePack" Version="2.2.85" /> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.2" /> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.2" /> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.4" /> + <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.4" /> + <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" /> - <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="ppy.osu.Framework" Version="2021.220.0" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="ppy.osu.Framework" Version="2021.329.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" /> - <PackageReference Include="Sentry" Version="3.0.1" /> - <PackageReference Include="SharpCompress" Version="0.27.1" /> + <PackageReference Include="Sentry" Version="3.2.0" /> + <PackageReference Include="SharpCompress" Version="0.28.1" /> <PackageReference Include="NUnit" Version="3.13.1" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> </ItemGroup> diff --git a/osu.iOS.props b/osu.iOS.props index 99ab88a064..ceb46eae87 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,19 +70,21 @@ <Reference Include="System.Net.Http" /> </ItemGroup> <ItemGroup Label="Package References"> - <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.220.0" /> + <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.329.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" /> </ItemGroup> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <PropertyGroup> <NoWarn>$(NoWarn);NU1605</NoWarn> </PropertyGroup> + <!-- Workaround to make SignalR 5.x work properly, avoiding a runtime error (https://github.com/mono/mono/issues/20805#issuecomment-791440473) --> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.0.3" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.3" /> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.0.3" /> - <PackageReference Include="MessagePack" Version="1.7.3.7" /> - <PackageReference Include="MessagePack.Annotations" Version="2.2.85" /> + <PackageReference Include="System.Memory" Version="4.5.4"> + <IncludeAssets>none</IncludeAssets> + </PackageReference> + <PackageReference Include="System.Buffers" Version="4.5.1"> + <IncludeAssets>none</IncludeAssets> + </PackageReference> </ItemGroup> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <ItemGroup Label="Transitive Dependencies"> @@ -91,8 +93,8 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> - <PackageReference Include="ppy.osu.Framework" Version="2021.220.0" /> - <PackageReference Include="SharpCompress" Version="0.27.1" /> + <PackageReference Include="ppy.osu.Framework" Version="2021.329.0" /> + <PackageReference Include="SharpCompress" Version="0.28.1" /> <PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />