diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ce353d9b27..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,2 +0,0 @@ -language: csharp -solution: osu.sln \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 17c0db12e7..f7c19064b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,7 @@ GEM public_suffix (>= 2.0.2, < 4.0) atomos (0.1.3) babosa (1.0.2) - claide (1.0.2) + claide (1.0.3) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) @@ -14,11 +14,11 @@ GEM declarative (0.0.10) declarative-option (0.1.0) digest-crc (0.4.1) - domain_name (0.5.20180417) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.1) + dotenv (2.7.5) emoji_regex (1.0.1) - excon (0.62.0) + excon (0.66.0) faraday (0.15.4) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) @@ -27,7 +27,7 @@ GEM faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) fastimage (2.1.5) - fastlane (2.117.0) + fastlane (2.129.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -46,8 +46,8 @@ GEM google-cloud-storage (>= 1.15.0, < 2.0.0) highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) - mini_magick (~> 4.5.1) - multi_json + jwt (~> 2.1.0) + mini_magick (>= 4.9.4, < 5.0.0) multi_xml (~> 0.5) multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) @@ -56,15 +56,15 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) - terminal-notifier (>= 1.6.2, < 2.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) - xcodeproj (>= 1.6.0, < 2.0.0) + xcodeproj (>= 1.8.1, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-clean_testflight_testers (0.2.0) + fastlane-plugin-clean_testflight_testers (0.3.0) fastlane-plugin-souyuz (0.8.1) souyuz (>= 0.8.1) fastlane-plugin-xamarin (0.6.3) @@ -79,7 +79,7 @@ GEM signet (~> 0.9) google-cloud-core (1.3.0) google-cloud-env (~> 1.0) - google-cloud-env (1.0.5) + google-cloud-env (1.2.0) faraday (~> 0.11) google-cloud-storage (1.16.0) digest-crc (~> 0.4) @@ -102,17 +102,17 @@ GEM memoist (0.16.0) mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - mini_magick (4.5.1) + mime-types-data (3.2019.0331) + mini_magick (4.9.5) mini_portile2 (2.4.0) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) nanaimo (0.2.6) naturally (2.2.0) - nokogiri (1.10.1) + nokogiri (1.10.4) mini_portile2 (~> 2.4.0) - os (1.0.0) + os (1.0.1) plist (3.5.0) public_suffix (2.0.5) representable (3.0.4) @@ -121,7 +121,7 @@ GEM uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) - rubyzip (1.2.2) + rubyzip (1.2.3) security (0.1.3) signet (0.11.0) addressable (~> 2.3) @@ -136,20 +136,20 @@ GEM fastlane (>= 2.29.0) highline (~> 1.7) nokogiri (~> 1.7) - terminal-notifier (1.8.0) + terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - tty-cursor (0.6.1) - tty-screen (0.6.5) - tty-spinner (0.9.0) - tty-cursor (~> 0.6.0) + tty-cursor (0.7.0) + tty-screen (0.7.0) + tty-spinner (0.9.1) + tty-cursor (~> 0.7) uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.5) - unicode-display_width (1.4.1) + unf_ext (0.0.7.6) + unicode-display_width (1.6.0) word_wrap (1.0.0) - xcodeproj (1.8.1) + xcodeproj (1.12.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/README.md b/README.md index 2c330e403c..56491a4be4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

# osu! @@ -19,8 +19,9 @@ Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh ## Requirements - A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed. +- When running on linux, please have a system-wide ffmpeg installation available to support video decoding. +- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs. - When working with the codebase, we recommend using an IDE with intellisense and syntax highlighting, such as [Visual Studio 2017+](https://visualstudio.microsoft.com/vs/), [Jetbrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). -- Note that there are **[additional requirements for Windows 7 and Windows 8.1](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore2x)** which you may need to manually install if your operating system is not up-to-date. ## Running osu! @@ -34,7 +35,7 @@ If you are not interested in developing the game, you can still consume our [bin | ------------- | ------------- | - **Linux** users are recommended to self-compile until we have official deployment in place. -- **iOS** users can join the [TestFlight beta program](https://t.co/PasE1zrHhw) (note that due to high demand this is regularly full). +- **iOS** users can join the [TestFlight beta program](https://testflight.apple.com/join/2tLcjWlF) (note that due to high demand this is regularly full). - **Android** users can self-compile, and expect a public beta soon. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. @@ -100,7 +101,7 @@ Before starting, please make sure you are familiar with the [development and tes Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as pain-free as possible. -For those interested, we love to reward quality contributions via bounties, paid out via paypal or osu! supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. +For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via paypal or osu! supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. ## Licence diff --git a/assets/lazer.png b/assets/lazer.png index 075a8e7184..1e40e844cc 100644 Binary files a/assets/lazer.png and b/assets/lazer.png differ diff --git a/build/cakebuild.csproj b/build/cakebuild.csproj index 8ccce35e26..d5a6d44740 100644 --- a/build/cakebuild.csproj +++ b/build/cakebuild.csproj @@ -5,7 +5,7 @@ netcoreapp2.0 - - + + \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 48c16caf0f..0b60e28b0f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,22 +1,6 @@ update_fastlane -default_platform(:ios) - platform :ios do - lane :testflight_prune_dry do - clean_testflight_testers(days_of_inactivity:45, dry_run: true) - end - - # Specify a custom number for what's "inactive" - lane :testflight_prune do - clean_testflight_testers(days_of_inactivity: 45) # 120 days, so about 4 months - end - - lane :update_version do |options| - options[:plist_path] = '../osu.iOS/Info.plist' - app_version(options) - end - desc 'Deploy to testflight' lane :beta do |options| update_version(options) @@ -62,4 +46,17 @@ platform :ios do match(options) end + + lane :update_version do |options| + options[:plist_path] = '../osu.iOS/Info.plist' + app_version(options) + end + + lane :testflight_prune_dry do + clean_testflight_testers(days_of_inactivity:45, dry_run: true) + end + + lane :testflight_prune do + clean_testflight_testers(days_of_inactivity: 45) + end end diff --git a/fastlane/README.md b/fastlane/README.md index 53bbc62cae..fbccf1c8c0 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -16,21 +16,6 @@ or alternatively using `brew cask install fastlane` # Available Actions ## iOS -### ios testflight_prune_dry -``` -fastlane ios testflight_prune_dry -``` - -### ios testflight_prune -``` -fastlane ios testflight_prune -``` - -### ios update_version -``` -fastlane ios update_version -``` - ### ios beta ``` fastlane ios beta @@ -46,6 +31,21 @@ Compile the project fastlane ios provision ``` Install provisioning profiles using match +### ios update_version +``` +fastlane ios update_version +``` + +### ios testflight_prune_dry +``` +fastlane ios testflight_prune_dry +``` + +### ios testflight_prune +``` +fastlane ios testflight_prune +``` + ---- diff --git a/osu.Android.props b/osu.Android.props index 5ee0573c58..0f6e32d664 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -49,9 +49,7 @@ osu.licenseheader - - Always - + @@ -62,7 +60,7 @@ - - + + diff --git a/osu.Android/Resources/drawable/lazer.png b/osu.Android/Resources/drawable/lazer.png index 075a8e7184..fc7aa8a092 100644 Binary files a/osu.Android/Resources/drawable/lazer.png and b/osu.Android/Resources/drawable/lazer.png differ diff --git a/osu.Android/lib/arm64-v8a/libbass.so b/osu.Android/lib/arm64-v8a/libbass.so deleted file mode 100644 index d5c24b3e4b..0000000000 Binary files a/osu.Android/lib/arm64-v8a/libbass.so and /dev/null differ diff --git a/osu.Android/lib/arm64-v8a/libbass_fx.so b/osu.Android/lib/arm64-v8a/libbass_fx.so deleted file mode 100644 index e498b10117..0000000000 Binary files a/osu.Android/lib/arm64-v8a/libbass_fx.so and /dev/null differ diff --git a/osu.Android/lib/armeabi-v7a/libbass.so b/osu.Android/lib/armeabi-v7a/libbass.so deleted file mode 100644 index f7d23b9052..0000000000 Binary files a/osu.Android/lib/armeabi-v7a/libbass.so and /dev/null differ diff --git a/osu.Android/lib/armeabi-v7a/libbass_fx.so b/osu.Android/lib/armeabi-v7a/libbass_fx.so deleted file mode 100644 index 006e2feb30..0000000000 Binary files a/osu.Android/lib/armeabi-v7a/libbass_fx.so and /dev/null differ diff --git a/osu.Android/lib/x86/libbass.so b/osu.Android/lib/x86/libbass.so deleted file mode 100644 index b0f758a42b..0000000000 Binary files a/osu.Android/lib/x86/libbass.so and /dev/null differ diff --git a/osu.Android/lib/x86/libbass_fx.so b/osu.Android/lib/x86/libbass_fx.so deleted file mode 100644 index 526dca39ca..0000000000 Binary files a/osu.Android/lib/x86/libbass_fx.so and /dev/null differ diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 975b7f9f5a..761f52f961 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -38,9 +38,9 @@ namespace osu.Desktop if (Host is DesktopGameHost desktopHost) return new StableStorage(desktopHost); } - catch (Exception e) + catch (Exception) { - Logger.Error(e, "Error while searching for stable install"); + Logger.Log("Could not find a stable install", LoggingTarget.Runtime, LogLevel.Important); } return null; @@ -52,11 +52,7 @@ namespace osu.Desktop if (!noVersionOverlay) { - LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, v => - { - Add(v); - v.Show(); - }); + LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) Add(new SquirrelUpdateManager()); @@ -71,7 +67,7 @@ namespace osu.Desktop switch (newScreen) { - case Intro _: + case IntroScreen _: case MainMenu _: versionManager?.Show(); break; diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index 1f1d2cea5f..51e801c185 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -113,13 +113,14 @@ namespace osu.Desktop.Overlays } [BackgroundDependencyLoader] - private void load(OsuColour colours, ChangelogOverlay changelog) + private void load(OsuColour colours, ChangelogOverlay changelog, NotificationOverlay notificationOverlay) { Icon = FontAwesome.Solid.CheckSquare; IconBackgound.Colour = colours.BlueDark; Activated = delegate { + notificationOverlay.Hide(); changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); return true; }; diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index cb488fea52..141b2cdbbc 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -29,29 +29,36 @@ namespace osu.Desktop if (!host.IsPrimaryInstance) { - var importer = new ArchiveImportIPCChannel(host); - // Restore the cwd so relative paths given at the command line work correctly - Directory.SetCurrentDirectory(cwd); - - foreach (var file in args) + if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args { - Console.WriteLine(@"Importing {0}", file); - if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000)) - throw new TimeoutException(@"IPC took too long to send"); + var importer = new ArchiveImportIPCChannel(host); + // Restore the cwd so relative paths given at the command line work correctly + Directory.SetCurrentDirectory(cwd); + + foreach (var file in args) + { + Console.WriteLine(@"Importing {0}", file); + if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000)) + throw new TimeoutException(@"IPC took too long to send"); + } + + return 0; } + + // we want to allow multiple instances to be started when in debug. + if (!DebugUtils.IsDebugBuild) + return 0; } - else - { - switch (args.FirstOrDefault() ?? string.Empty) - { - default: - host.Run(new OsuGameDesktop(args)); - break; - case "--tournament": - host.Run(new TournamentGame()); - break; - } + switch (args.FirstOrDefault() ?? string.Empty) + { + default: + host.Run(new OsuGameDesktop(args)); + break; + + case "--tournament": + host.Run(new TournamentGame()); + break; } return 0; diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 78a1e680ec..fa41c061b5 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -27,6 +27,8 @@ namespace osu.Desktop.Updater public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); + private static readonly Logger logger = Logger.GetLogger("updater"); + [BackgroundDependencyLoader] private void load(NotificationOverlay notification, OsuGameBase game) { @@ -77,7 +79,7 @@ namespace osu.Desktop.Updater { if (useDeltaPatching) { - Logger.Error(e, @"delta patching failed!"); + logger.Add(@"delta patching failed; will attempt full download!"); //could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) //try again without deltas. @@ -163,16 +165,11 @@ namespace osu.Desktop.Updater { public LogLevel Level { get; set; } = LogLevel.Info; - private Logger logger; - public void Write(string message, LogLevel logLevel) { if (logLevel < Level) return; - if (logger == null) - logger = Logger.GetLogger("updater"); - logger.Add(message); } diff --git a/osu.Desktop/lazer.ico b/osu.Desktop/lazer.ico old mode 100644 new mode 100755 index 0c894dca41..a6aa8abb9f Binary files a/osu.Desktop/lazer.ico and b/osu.Desktop/lazer.ico differ diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 8c9e1f279f..538aaf2d7a 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -28,8 +28,8 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj index 37e7c45a4e..7990c35e09 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj +++ b/osu.Game.Rulesets.Catch.Tests.iOS/osu.Game.Rulesets.Catch.Tests.iOS.csproj @@ -13,14 +13,6 @@ - - libbass.a - PreserveNewest - - - libbass_fx.a - PreserveNewest - Linker.xml diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index e45ed8c6f4..493ac7ae39 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.MathUtils; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; @@ -22,10 +23,10 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("spinner")] [TestCase("spinner-and-circles")] [TestCase("slider")] - public new void Test(string name) - { - base.Test(name); - } + [TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })] + [TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })] + [TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })] + public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 33f93cdb4a..406c0af28d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites; using osuTK.Graphics; using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Textures; +using osu.Game.Audio; namespace osu.Game.Rulesets.Catch.Tests { @@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Tests return null; } - public SampleChannel GetSample(string sampleName) => + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); public Texture GetTexture(string componentName) => 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 9acf47a67c..7c282f449b 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 @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 645cb5701a..5ab47c1611 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; using osuTK; using osu.Game.Rulesets.Catch.MathUtils; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { base.PostProcess(); - applyPositionOffsets(); + ApplyPositionOffsets(Beatmap); initialiseHyperDash((List)Beatmap.HitObjects); @@ -40,19 +41,29 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } } - private void applyPositionOffsets() + public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods) { var rng = new FastRandom(RNG_SEED); - // todo: HardRock displacement should be applied here - foreach (var obj in Beatmap.HitObjects) + bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock); + float? lastPosition = null; + double lastStartTime = 0; + + foreach (var obj in beatmap.HitObjects.OfType()) { + obj.XOffset = 0; + switch (obj) { + case Fruit fruit: + if (shouldApplyHardRockOffset) + applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng); + break; + case BananaShower bananaShower: foreach (var banana in bananaShower.NestedHitObjects.OfType()) { - banana.X = (float)rng.NextDouble(); + banana.XOffset = (float)rng.NextDouble(); rng.Next(); // osu!stable retrieved a random banana type rng.Next(); // osu!stable retrieved a random banana rotation rng.Next(); // osu!stable retrieved a random banana colour @@ -63,12 +74,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps case JuiceStream juiceStream: foreach (var nested in juiceStream.NestedHitObjects) { - var hitObject = (CatchHitObject)nested; - if (hitObject is TinyDroplet) - hitObject.X += rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH; - else if (hitObject is Droplet) + var catchObject = (CatchHitObject)nested; + catchObject.XOffset = 0; + + if (catchObject is TinyDroplet) + catchObject.XOffset = MathHelper.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X); + else if (catchObject is Droplet) rng.Next(); // osu!stable retrieved a random droplet rotation - hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1); } break; @@ -76,6 +88,105 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } } + private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) + { + if (hitObject is JuiceStream stream) + { + lastPosition = stream.EndX; + lastStartTime = stream.EndTime; + return; + } + + if (!(hitObject is Fruit)) + return; + + float offsetPosition = hitObject.X; + double startTime = hitObject.StartTime; + + if (lastPosition == null) + { + lastPosition = offsetPosition; + lastStartTime = startTime; + + return; + } + + float positionDiff = offsetPosition - lastPosition.Value; + double timeDiff = startTime - lastStartTime; + + if (timeDiff > 1000) + { + lastPosition = offsetPosition; + lastStartTime = startTime; + return; + } + + if (positionDiff == 0) + { + applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng); + hitObject.XOffset = offsetPosition - hitObject.X; + return; + } + + if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d) + applyOffset(ref offsetPosition, positionDiff); + + hitObject.XOffset = offsetPosition - hitObject.X; + + lastPosition = offsetPosition; + lastStartTime = startTime; + } + + /// + /// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield. + /// + /// The position which the offset should be applied to. + /// The maximum offset, cannot exceed 20px. + /// The random number generator. + private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng) + { + bool right = rng.NextBool(); + float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH; + + if (right) + { + // Clamp to the right bound + if (position + rand <= 1) + position += rand; + else + position -= rand; + } + else + { + // Clamp to the left bound + if (position - rand >= 0) + position -= rand; + else + position += rand; + } + } + + /// + /// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield. + /// + /// The position which the offset should be applied to. + /// The amount to offset by. + private static void applyOffset(ref float position, float amount) + { + if (amount > 0) + { + // Clamp to the right bound + if (position + amount < 1) + position += amount; + } + else + { + // Clamp to the left bound + if (position + amount > 0) + position += amount; + } + } + private void initialiseHyperDash(List objects) { List objectWithDroplets = new List(); diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ea9f225cc1..6f1a7873ec 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -11,7 +11,6 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; @@ -108,7 +107,7 @@ namespace osu.Game.Rulesets.Catch case ModType.Fun: return new Mod[] { - new MultiMod(new ModWindUp(), new ModWindDown()) + new MultiMod(new ModWindUp(), new ModWindDown()) }; default: diff --git a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs index b3605b013b..c721ff862a 100644 --- a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs +++ b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs @@ -61,6 +61,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils /// The random value. public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound)); + /// + /// Generates a random integer value within the range [, ). + /// + /// The lower bound of the range. + /// The upper bound of the range. + /// The random value. + public int Next(double lowerBound, double upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound)); + /// /// Generates a random double value within the range [0, 1). /// diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index 060e51e31d..ced1900ba9 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs @@ -1,121 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.MathUtils; -using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; -using System; -using osu.Game.Rulesets.Objects; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModHardRock : ModHardRock, IApplicableToHitObject + public class CatchModHardRock : ModHardRock, IApplicableToBeatmap { public override double ScoreMultiplier => 1.12; public override bool Ranked => true; - private float? lastPosition; - private double lastStartTime; - - public void ApplyToHitObject(HitObject hitObject) - { - if (hitObject is JuiceStream stream) - { - lastPosition = stream.EndX; - lastStartTime = stream.EndTime; - return; - } - - if (!(hitObject is Fruit)) - return; - - var catchObject = (CatchHitObject)hitObject; - - float position = catchObject.X; - double startTime = hitObject.StartTime; - - if (lastPosition == null) - { - lastPosition = position; - lastStartTime = startTime; - - return; - } - - float positionDiff = position - lastPosition.Value; - double timeDiff = startTime - lastStartTime; - - if (timeDiff > 1000) - { - lastPosition = position; - lastStartTime = startTime; - return; - } - - if (positionDiff == 0) - { - applyRandomOffset(ref position, timeDiff / 4d); - catchObject.X = position; - return; - } - - if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d) - applyOffset(ref position, positionDiff); - - catchObject.X = position; - - lastPosition = position; - lastStartTime = startTime; - } - - /// - /// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield. - /// - /// The position which the offset should be applied to. - /// The maximum offset, cannot exceed 20px. - private void applyRandomOffset(ref float position, double maxOffset) - { - bool right = RNG.NextBool(); - float rand = Math.Min(20, (float)RNG.NextDouble(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH; - - if (right) - { - // Clamp to the right bound - if (position + rand <= 1) - position += rand; - else - position -= rand; - } - else - { - // Clamp to the left bound - if (position - rand >= 0) - position -= rand; - else - position += rand; - } - } - - /// - /// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield. - /// - /// The position which the offset should be applied to. - /// The amount to offset by. - private void applyOffset(ref float position, float amount) - { - if (amount > 0) - { - // Clamp to the right bound - if (position + amount < 1) - position += amount; - } - else - { - // Clamp to the left bound - if (position + amount > 0) - position += amount; - } - } + public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index be76edc01b..19a1b59752 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -3,6 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -12,7 +13,18 @@ namespace osu.Game.Rulesets.Catch.Objects { public const double OBJECT_RADIUS = 44; - public float X { get; set; } + private float x; + + public float X + { + get => x + XOffset; + set => x = value; + } + + /// + /// A random offset applied to , set by the . + /// + internal float XOffset { get; set; } public double TimePreempt = 1000; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 5785d9a9ca..00734810b3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -3,12 +3,10 @@ using System; using osuTK; -using osuTK.Graphics; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; -using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -60,19 +58,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); } - protected override void SkinChanged(ISkinSource skin, bool allowFallback) + protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; + + protected override void UpdateInitialTransforms() => this.FadeInFromZero(200); + + protected override void UpdateStateTransforms(ArmedState state) { - base.SkinChanged(skin, allowFallback); - - if (HitObject is IHasComboInformation combo) - AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; - } - - protected override void UpdateState(ArmedState state) - { - using (BeginAbsoluteSequence(HitObject.StartTime - HitObject.TimePreempt)) - this.FadeIn(200); - var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; using (BeginAbsoluteSequence(endTime, true)) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs index 9cabdc3dd9..059310d671 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -27,16 +26,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable private void load() { AddInternal(pulp = new Pulp { Size = Size }); - } - public override Color4 AccentColour - { - get => base.AccentColour; - set - { - base.AccentColour = value; - pulp.AccentColour = AccentColour; - } + AccentColour.BindValueChanged(colour => { pulp.AccentColour = colour.NewValue; }, true); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs index 77407def54..1af77b75fc 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable private void load() { // todo: this should come from the skin. - AccentColour = colourForRepresentation(HitObject.VisualRepresentation); + AccentColour.Value = colourForRepresentation(HitObject.VisualRepresentation); AddRangeInternal(new[] { @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Hollow = !HitObject.HyperDash, Type = EdgeEffectType.Glow, Radius = 4 * radius_adjust, - Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Darken(1).Opacity(0.6f) + Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Value.Darken(1).Opacity(0.6f) }, Size = new Vector2(Height), Anchor = Anchor.Centre, @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable new Box { AlwaysPresent = true, - Colour = AccentColour, + Colour = AccentColour.Value, Alpha = 0, RelativeSizeAxes = Axes.Both } @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Anchor = Anchor.Centre, Origin = Anchor.Centre, AccentColour = Color4.Red, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0.5f, Scale = new Vector2(1.333f) }); @@ -115,32 +115,32 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(small_pulp), Y = -0.34f, }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4), Position = positionAt(0, distance_from_centre_4), }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4), Position = positionAt(90, distance_from_centre_4), }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4), Position = positionAt(180, distance_from_centre_4), }, new Pulp { Size = new Vector2(large_pulp_4), - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Position = positionAt(270, distance_from_centre_4), }, } @@ -154,32 +154,32 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(small_pulp), Y = -0.3f, }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4), Position = positionAt(45, distance_from_centre_4), }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4), Position = positionAt(135, distance_from_centre_4), }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4), Position = positionAt(225, distance_from_centre_4), }, new Pulp { Size = new Vector2(large_pulp_4), - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Position = positionAt(315, distance_from_centre_4), }, } @@ -193,26 +193,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(small_pulp), Y = -0.33f, }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_3), Position = positionAt(60, distance_from_centre_3), }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_3), Position = positionAt(180, distance_from_centre_3), }, new Pulp { Size = new Vector2(large_pulp_3), - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Position = positionAt(300, distance_from_centre_3), }, } @@ -226,26 +226,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(small_pulp), Y = -0.25f, }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_3), Position = positionAt(0, distance_from_centre_3), }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_3), Position = positionAt(120, distance_from_centre_3), }, new Pulp { Size = new Vector2(large_pulp_3), - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Position = positionAt(240, distance_from_centre_3), }, } @@ -259,13 +259,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(small_pulp), Y = -0.3f }, new Pulp { - AccentColour = AccentColour, + AccentColour = AccentColour.Value, Size = new Vector2(large_pulp_4 * 0.8f, large_pulp_4 * 2.5f), Y = 0.05f, }, diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs index b9b6d5b924..1e9daf18db 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces Anchor = Anchor.Centre; Origin = Anchor.Centre; - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; Colour = Color4.White.Opacity(0.9f); } diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json new file mode 100644 index 0000000000..83f9e30800 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider-expected-conversion.json @@ -0,0 +1,150 @@ +{ + "Mappings": [{ + "StartTime": 369, + "Objects": [{ + "StartTime": 369, + "Position": 177 + }, + { + "StartTime": 450, + "Position": 216.539276 + }, + { + "StartTime": 532, + "Position": 256.5667 + }, + { + "StartTime": 614, + "Position": 296.594116 + }, + { + "StartTime": 696, + "Position": 336.621521 + }, + { + "StartTime": 778, + "Position": 376.99762 + }, + { + "StartTime": 860, + "Position": 337.318878 + }, + { + "StartTime": 942, + "Position": 297.291443 + }, + { + "StartTime": 1024, + "Position": 257.264038 + }, + { + "StartTime": 1106, + "Position": 217.2366 + }, + { + "StartTime": 1188, + "Position": 177 + }, + { + "StartTime": 1270, + "Position": 216.818192 + }, + { + "StartTime": 1352, + "Position": 256.8456 + }, + { + "StartTime": 1434, + "Position": 296.873047 + }, + { + "StartTime": 1516, + "Position": 336.900452 + }, + { + "StartTime": 1598, + "Position": 376.99762 + }, + { + "StartTime": 1680, + "Position": 337.039948 + }, + { + "StartTime": 1762, + "Position": 297.0125 + }, + { + "StartTime": 1844, + "Position": 256.9851 + }, + { + "StartTime": 1926, + "Position": 216.957672 + }, + { + "StartTime": 2008, + "Position": 177 + }, + { + "StartTime": 2090, + "Position": 217.097137 + }, + { + "StartTime": 2172, + "Position": 257.124573 + }, + { + "StartTime": 2254, + "Position": 297.152 + }, + { + "StartTime": 2336, + "Position": 337.179443 + }, + { + "StartTime": 2418, + "Position": 376.99762 + }, + { + "StartTime": 2500, + "Position": 336.760956 + }, + { + "StartTime": 2582, + "Position": 296.733643 + }, + { + "StartTime": 2664, + "Position": 256.7062 + }, + { + "StartTime": 2746, + "Position": 216.678772 + }, + { + "StartTime": 2828, + "Position": 177 + }, + { + "StartTime": 2909, + "Position": 216.887909 + }, + { + "StartTime": 2991, + "Position": 256.915344 + }, + { + "StartTime": 3073, + "Position": 296.942749 + }, + { + "StartTime": 3155, + "Position": 336.970184 + }, + { + "StartTime": 3237, + "Position": 376.99762 + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu new file mode 100644 index 0000000000..c1971edf2c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-repeat-slider.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8 +SliderMultiplier:1.6 +SliderTickRate:4 + +[TimingPoints] +369,327.868852459016,4,2,2,32,1,0 + +[HitObjects] +177,191,369,6,0,L|382:192,7,200 diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json new file mode 100644 index 0000000000..7333b600fb --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner-expected-conversion.json @@ -0,0 +1,74 @@ +{ + "Mappings": [{ + "StartTime": 369, + "Objects": [{ + "StartTime": 369, + "Position": 65 + }, + { + "StartTime": 450, + "Position": 482 + }, + { + "StartTime": 532, + "Position": 164 + }, + { + "StartTime": 614, + "Position": 315 + }, + { + "StartTime": 696, + "Position": 145 + }, + { + "StartTime": 778, + "Position": 159 + }, + { + "StartTime": 860, + "Position": 310 + }, + { + "StartTime": 942, + "Position": 441 + }, + { + "StartTime": 1024, + "Position": 428 + }, + { + "StartTime": 1106, + "Position": 243 + }, + { + "StartTime": 1188, + "Position": 422 + }, + { + "StartTime": 1270, + "Position": 481 + }, + { + "StartTime": 1352, + "Position": 104 + }, + { + "StartTime": 1434, + "Position": 473 + }, + { + "StartTime": 1516, + "Position": 135 + }, + { + "StartTime": 1598, + "Position": 360 + }, + { + "StartTime": 1680, + "Position": 123 + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu new file mode 100644 index 0000000000..208d21a344 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-spinner.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8 +SliderMultiplier:1.6 +SliderTickRate:4 + +[TimingPoints] +369,327.868852459016,4,2,2,32,1,0 + +[HitObjects] +256,192,369,12,0,1680,0:0:0:0: diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json new file mode 100644 index 0000000000..bbc16ab912 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream-expected-conversion.json @@ -0,0 +1,234 @@ +{ + "Mappings": [{ + "StartTime": 369, + "Objects": [{ + "StartTime": 369, + "Position": 258 + }] + }, + { + "StartTime": 450, + "Objects": [{ + "StartTime": 450, + "Position": 254 + }] + }, + { + "StartTime": 532, + "Objects": [{ + "StartTime": 532, + "Position": 241 + }] + }, + { + "StartTime": 614, + "Objects": [{ + "StartTime": 614, + "Position": 238 + }] + }, + { + "StartTime": 696, + "Objects": [{ + "StartTime": 696, + "Position": 238 + }] + }, + { + "StartTime": 778, + "Objects": [{ + "StartTime": 778, + "Position": 278 + }] + }, + { + "StartTime": 860, + "Objects": [{ + "StartTime": 860, + "Position": 238 + }] + }, + { + "StartTime": 942, + "Objects": [{ + "StartTime": 942, + "Position": 278 + }] + }, + { + "StartTime": 1024, + "Objects": [{ + "StartTime": 1024, + "Position": 238 + }] + }, + { + "StartTime": 1106, + "Objects": [{ + "StartTime": 1106, + "Position": 278 + }] + }, + { + "StartTime": 1188, + "Objects": [{ + "StartTime": 1188, + "Position": 278 + }] + }, + { + "StartTime": 1270, + "Objects": [{ + "StartTime": 1270, + "Position": 278 + }] + }, + { + "StartTime": 1352, + "Objects": [{ + "StartTime": 1352, + "Position": 238 + }] + }, + { + "StartTime": 1434, + "Objects": [{ + "StartTime": 1434, + "Position": 258 + }] + }, + { + "StartTime": 1516, + "Objects": [{ + "StartTime": 1516, + "Position": 253 + }] + }, + { + "StartTime": 1598, + "Objects": [{ + "StartTime": 1598, + "Position": 238 + }] + }, + { + "StartTime": 1680, + "Objects": [{ + "StartTime": 1680, + "Position": 260 + }] + }, + { + "StartTime": 1762, + "Objects": [{ + "StartTime": 1762, + "Position": 238 + }] + }, + { + "StartTime": 1844, + "Objects": [{ + "StartTime": 1844, + "Position": 278 + }] + }, + { + "StartTime": 1926, + "Objects": [{ + "StartTime": 1926, + "Position": 278 + }] + }, + { + "StartTime": 2008, + "Objects": [{ + "StartTime": 2008, + "Position": 238 + }] + }, + { + "StartTime": 2090, + "Objects": [{ + "StartTime": 2090, + "Position": 238 + }] + }, + { + "StartTime": 2172, + "Objects": [{ + "StartTime": 2172, + "Position": 243 + }] + }, + { + "StartTime": 2254, + "Objects": [{ + "StartTime": 2254, + "Position": 278 + }] + }, + { + "StartTime": 2336, + "Objects": [{ + "StartTime": 2336, + "Position": 278 + }] + }, + { + "StartTime": 2418, + "Objects": [{ + "StartTime": 2418, + "Position": 238 + }] + }, + { + "StartTime": 2500, + "Objects": [{ + "StartTime": 2500, + "Position": 258 + }] + }, + { + "StartTime": 2582, + "Objects": [{ + "StartTime": 2582, + "Position": 256 + }] + }, + { + "StartTime": 2664, + "Objects": [{ + "StartTime": 2664, + "Position": 242 + }] + }, + { + "StartTime": 2746, + "Objects": [{ + "StartTime": 2746, + "Position": 238 + }] + }, + { + "StartTime": 2828, + "Objects": [{ + "StartTime": 2828, + "Position": 238 + }] + }, + { + "StartTime": 2909, + "Objects": [{ + "StartTime": 2909, + "Position": 271 + }] + }, + { + "StartTime": 2991, + "Objects": [{ + "StartTime": 2991, + "Position": 254 + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu new file mode 100644 index 0000000000..321af4604b --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/hardrock-stream.osu @@ -0,0 +1,50 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +369,327.868852459016,4,2,2,32,1,0 + +[HitObjects] +258,189,369,1,0,0:0:0:0: +258,189,450,1,0,0:0:0:0: +258,189,532,1,0,0:0:0:0: +258,189,614,1,0,0:0:0:0: +258,189,696,1,0,0:0:0:0: +258,189,778,1,0,0:0:0:0: +258,189,860,1,0,0:0:0:0: +258,189,942,1,0,0:0:0:0: +258,189,1024,1,0,0:0:0:0: +258,189,1106,1,0,0:0:0:0: +258,189,1188,1,0,0:0:0:0: +258,189,1270,1,0,0:0:0:0: +258,189,1352,1,0,0:0:0:0: +258,189,1434,1,0,0:0:0:0: +258,189,1516,1,0,0:0:0:0: +258,189,1598,1,0,0:0:0:0: +258,189,1680,1,0,0:0:0:0: +258,189,1762,1,0,0:0:0:0: +258,189,1844,1,0,0:0:0:0: +258,189,1926,1,0,0:0:0:0: +258,189,2008,1,0,0:0:0:0: +258,189,2090,1,0,0:0:0:0: +258,189,2172,1,0,0:0:0:0: +258,189,2254,1,0,0:0:0:0: +258,189,2336,1,0,0:0:0:0: +258,189,2418,1,0,0:0:0:0: +258,189,2500,1,0,0:0:0:0: +258,189,2582,1,0,0:0:0:0: +258,189,2664,1,0,0:0:0:0: +258,189,2746,1,0,0:0:0:0: +258,189,2828,1,0,0:0:0:0: +258,189,2909,1,0,0:0:0:0: +258,189,2991,1,0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 0b06e958e6..ceda643335 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI if (lastPlateableFruit == null) return; - // this is required to make this run after the last caught fruit runs UpdateState at least once. + // this is required to make this run after the last caught fruit runs updateState() at least once. // TODO: find a better alternative if (lastPlateableFruit.IsLoaded) action(); @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Catch.UI additive.Scale = Scale; additive.Colour = HyperDashing ? Color4.Red : Color4.White; additive.RelativePositionAxes = RelativePositionAxes; - additive.Blending = BlendingMode.Additive; + additive.Blending = BlendingParameters.Additive; AdditiveTarget.Add(additive); diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj index 24abccb19d..58c2e2aa5a 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj +++ b/osu.Game.Rulesets.Mania.Tests.iOS/osu.Game.Rulesets.Mania.Tests.iOS.csproj @@ -13,14 +13,6 @@ - - libbass.a - PreserveNewest - - - libbass_fx.a - PreserveNewest - Linker.xml diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index 6b95975059..6f10540973 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -20,10 +20,7 @@ namespace osu.Game.Rulesets.Mania.Tests protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; [TestCase("basic")] - public new void Test(string name) - { - base.Test(name); - } + public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) { @@ -35,11 +32,37 @@ namespace osu.Game.Rulesets.Mania.Tests }; } - protected override ManiaConvertMapping CreateConvertMapping() => new ManiaConvertMapping(Converter); + private readonly Dictionary rngSnapshots = new Dictionary(); + + protected override void OnConversionGenerated(HitObject original, IEnumerable result, IBeatmapConverter beatmapConverter) + { + base.OnConversionGenerated(original, result, beatmapConverter); + + rngSnapshots[original] = new RngSnapshot(beatmapConverter); + } + + protected override ManiaConvertMapping CreateConvertMapping(HitObject source) => new ManiaConvertMapping(rngSnapshots[source]); protected override Ruleset CreateRuleset() => new ManiaRuleset(); } + public class RngSnapshot + { + public readonly uint RandomW; + public readonly uint RandomX; + public readonly uint RandomY; + public readonly uint RandomZ; + + public RngSnapshot(IBeatmapConverter converter) + { + var maniaConverter = (ManiaBeatmapConverter)converter; + RandomW = maniaConverter.Random.W; + RandomX = maniaConverter.Random.X; + RandomY = maniaConverter.Random.Y; + RandomZ = maniaConverter.Random.Z; + } + } + public class ManiaConvertMapping : ConvertMapping, IEquatable { public uint RandomW; @@ -51,13 +74,12 @@ namespace osu.Game.Rulesets.Mania.Tests { } - public ManiaConvertMapping(IBeatmapConverter converter) + public ManiaConvertMapping(RngSnapshot snapshot) { - var maniaConverter = (ManiaBeatmapConverter)converter; - RandomW = maniaConverter.Random.W; - RandomX = maniaConverter.Random.X; - RandomY = maniaConverter.Random.Y; - RandomZ = maniaConverter.Random.Z; + RandomW = snapshot.RandomW; + RandomX = snapshot.RandomX; + RandomY = snapshot.RandomY; + RandomZ = snapshot.RandomZ; } public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs index 04c5724f93..622d840a0c 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests Child = drawableObject = new DrawableHoldNote(holdNote) { Height = 300, - AccentColour = OsuColour.Gray(0.3f) + AccentColour = { Value = OsuColour.Gray(0.3f) } } }; } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index b2613a59d5..031abb08e2 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Tests AutoSizeAxes = Axes.Both, Child = new NoteContainer(direction, $"note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}") { - Child = hitObject = new DrawableNote(note) { AccentColour = Color4.OrangeRed } + Child = hitObject = new DrawableNote(note) { AccentColour = { Value = Color4.OrangeRed } } } }; } @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Mania.Tests Child = hitObject = new DrawableHoldNote(note) { RelativeSizeAxes = Axes.Both, - AccentColour = Color4.OrangeRed, + AccentColour = { Value = Color4.OrangeRed }, } } }; 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 df5131dd8b..4dcfc1b81f 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 @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs index a3cd455886..e4a28167ec 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// Keep the same as last row. /// - ForceStack = 1 << 0, + ForceStack = 1, /// /// Keep different from last row. diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index d83033f9c6..8966b5058f 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; @@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Mania case ModType.Fun: return new Mod[] { - new MultiMod(new ModWindUp(), new ModWindDown()) + new MultiMod(new ModWindUp(), new ModWindDown()) }; default: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs index 17f4098420..485595cea9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModMirror : Mod, IApplicableToBeatmap + public class ManiaModMirror : Mod, IApplicableToBeatmap { public override string Name => "Mirror"; public override string Acronym => "MR"; @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override bool Ranked => true; - public void ApplyToBeatmap(Beatmap beatmap) + public void ApplyToBeatmap(IBeatmap beatmap) { var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index ba16140644..9275371a61 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModRandom : Mod, IApplicableToBeatmap + public class ManiaModRandom : Mod, IApplicableToBeatmap { public override string Name => "Random"; public override string Acronym => "RD"; @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Description => @"Shuffle around the keys!"; public override double ScoreMultiplier => 1; - public void ApplyToBeatmap(Beatmap beatmap) + public void ApplyToBeatmap(IBeatmap beatmap) { var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => RNG.Next()).ToList(); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 9c3197504f..e9c352c97e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Alpha = 0.2f; } - protected override void UpdateState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 9368af987d..fc3b6885d7 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -6,9 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; -using osuTK.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; @@ -36,11 +36,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private bool hasBroken; - private readonly Container tickContainer; - public DrawableHoldNote(HoldNote hitObject) : base(hitObject) { + Container tickContainer; RelativeSizeAxes = Axes.X; AddRangeInternal(new Drawable[] @@ -74,6 +73,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AddNested(Head); AddNested(Tail); + + AccentColour.BindValueChanged(colour => + { + bodyPiece.AccentColour = colour.NewValue; + Head.AccentColour.Value = colour.NewValue; + Tail.AccentColour.Value = colour.NewValue; + tickContainer.ForEach(t => t.AccentColour.Value = colour.NewValue); + }, true); } protected override void OnDirectionChanged(ValueChangedEvent e) @@ -83,20 +90,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; } - public override Color4 AccentColour - { - get => base.AccentColour; - set - { - base.AccentColour = value; - - bodyPiece.AccentColour = value; - Head.AccentColour = value; - Tail.AccentColour = value; - tickContainer.ForEach(t => t.AccentColour = value); - } - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) @@ -112,6 +105,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; } + protected override void UpdateStateTransforms(ArmedState state) + { + using (BeginDelayedSequence(HitObject.Duration, true)) + base.UpdateStateTransforms(state); + } + protected void BeginHold() { holdStartTime = Time.Current; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 9a29273282..9b0322a6cd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -3,7 +3,6 @@ using System; using osuTK; -using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,11 +22,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public Func HoldStartTime; - private readonly Container glowContainer; - public DrawableHoldNoteTick(HoldNoteTick hitObject) : base(hitObject) { + Container glowContainer; + Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; @@ -53,23 +52,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } }); - } - public override Color4 AccentColour - { - get => base.AccentColour; - set + AccentColour.BindValueChanged(colour => { - base.AccentColour = value; - glowContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 2f, Roundness = 15f, - Colour = value.Opacity(0.3f) + Colour = colour.NewValue.Opacity(0.3f) }; - } + }, true); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 0873f753be..e5b114ca81 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -45,20 +45,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } - } - public abstract class DrawableManiaHitObject : DrawableManiaHitObject - where TObject : ManiaHitObject - { - public new readonly TObject HitObject; - - protected DrawableManiaHitObject(TObject hitObject) - : base(hitObject) - { - HitObject = hitObject; - } - - protected override void UpdateState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { switch (state) { @@ -72,4 +60,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } } + + public abstract class DrawableManiaHitObject : DrawableManiaHitObject + where TObject : ManiaHitObject + { + public new readonly TObject HitObject; + + protected DrawableManiaHitObject(TObject hitObject) + : base(hitObject) + { + HitObject = hitObject; + } + } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index afd7777861..dccff7f6ac 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Input.Bindings; @@ -30,6 +29,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Masking = true; AddInternal(headPiece = new NotePiece()); + + AccentColour.BindValueChanged(colour => + { + headPiece.AccentColour = colour.NewValue; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour.NewValue.Lighten(1f).Opacity(0.6f), + Radius = 10, + }; + }, true); } protected override void OnDirectionChanged(ValueChangedEvent e) @@ -39,23 +50,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables headPiece.Anchor = headPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } - public override Color4 AccentColour - { - get => base.AccentColour; - set - { - base.AccentColour = value; - headPiece.AccentColour = AccentColour; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = AccentColour.Lighten(1f).Opacity(0.6f), - Radius = 10, - }; - } - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs index 8102718edf..31a4857805 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -26,14 +26,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces public BodyPiece() { - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; Children = new[] { Background = new Box { RelativeSizeAxes = Axes.Both }, Foreground = new BufferedContainer { - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, CacheDrawnFrameBuffer = true, Children = new Drawable[] @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } - private Cached subtractionCache = new Cached(); + private readonly Cached subtractionCache = new Cached(); public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs index 9e0307c5c2..48c7ea7b7f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces Name = "Top", RelativeSizeAxes = Axes.Both, Height = 0.5f, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = ColourInfo.GradientVertical(Color4.Transparent, Color4.White.Opacity(alpha)) }, new Box @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, Height = 0.5f, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(alpha), Color4.Transparent) } }; diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index e5669816fa..7b8bbc2095 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.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; @@ -77,13 +78,37 @@ namespace osu.Game.Rulesets.Mania.Replays private IEnumerable generateActionPoints() { - foreach (var obj in Beatmap.HitObjects) + for (int i = 0; i < Beatmap.HitObjects.Count; i++) { - yield return new HitPoint { Time = obj.StartTime, Column = obj.Column }; - yield return new ReleasePoint { Time = ((obj as IHasEndTime)?.EndTime ?? obj.StartTime) + RELEASE_DELAY, Column = obj.Column }; + var currentObject = Beatmap.HitObjects[i]; + var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button + + double endTime = (currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime; + + bool canDelayKeyUp = nextObjectInColumn == null || + nextObjectInColumn.StartTime > endTime + RELEASE_DELAY; + + double calculatedDelay = canDelayKeyUp ? RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9; + + yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column }; + + yield return new ReleasePoint { Time = endTime + calculatedDelay, Column = currentObject.Column }; } } + protected override HitObject GetNextObject(int currentIndex) + { + int desiredColumn = Beatmap.HitObjects[currentIndex].Column; + + for (int i = currentIndex + 1; i < Beatmap.HitObjects.Count; i++) + { + if (Beatmap.HitObjects[i].Column == desiredColumn) + return Beatmap.HitObjects[i]; + } + + return null; + } + private interface IActionPoint { double Time { get; set; } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c59bed4ea7..91dd236ab1 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The DrawableHitObject to add. public override void Add(DrawableHitObject hitObject) { - hitObject.AccentColour = AccentColour; + hitObject.AccentColour.Value = AccentColour; hitObject.OnNewResult += OnNewResult; HitObjectContainer.Add(hitObject); diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs index b4e29ae9f9..5ee78aa496 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components Name = "Background Gradient Overlay", RelativeSizeAxes = Axes.Both, Height = 0.5f, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0 } }; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index c8aeda8fe4..0718de2c7d 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; + protected override bool RelativeScaleBeatLengths => true; + protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs index 0ec1fc38d2..48470add8b 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, judgedObject.AccentColour, Color4.White, 0, 1), + Colour = Interpolation.ValueAt(0.1f, judgedObject.AccentColour.Value, Color4.White, 0, 1), Radius = 100, }, Child = new Box diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj index 9930a166e3..c7787bd162 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj +++ b/osu.Game.Rulesets.Osu.Tests.iOS/osu.Game.Rulesets.Osu.Tests.iOS.csproj @@ -13,14 +13,6 @@ - - libbass.a - PreserveNewest - - - libbass_fx.a - PreserveNewest - Linker.xml diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index f98d63e6c7..e9fdf924c3 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -8,7 +8,6 @@ using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Osu.Tests @@ -21,10 +20,10 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase("basic")] [TestCase("colinear-perfect-curve")] [TestCase("slider-ticks")] - public new void Test(string name) - { - base.Test(name); - } + [TestCase("repeat-slider")] + [TestCase("uneven-repeat-slider")] + [TestCase("old-stacking")] + public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) { @@ -32,22 +31,22 @@ namespace osu.Game.Rulesets.Osu.Tests { case Slider slider: foreach (var nested in slider.NestedHitObjects) - yield return createConvertValue(nested); + yield return createConvertValue((OsuHitObject)nested); break; default: - yield return createConvertValue(hitObject); + yield return createConvertValue((OsuHitObject)hitObject); break; } - ConvertValue createConvertValue(HitObject obj) => new ConvertValue + ConvertValue createConvertValue(OsuHitObject obj) => new ConvertValue { StartTime = obj.StartTime, EndTime = (obj as IHasEndTime)?.EndTime ?? obj.StartTime, - X = (obj as IHasPosition)?.X ?? OsuPlayfield.BASE_SIZE.X / 2, - Y = (obj as IHasPosition)?.Y ?? OsuPlayfield.BASE_SIZE.Y / 2, + X = obj.StackedPosition.X, + Y = obj.StackedPosition.Y }; } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index e55dc1f902..693faee3b7 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Osu.Tests protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; [TestCase(6.931145117263422, "diffcalc-test")] + [TestCase(1.0736587013228804d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/approachcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/approachcircle@2x.png new file mode 100755 index 0000000000..db2f4a5730 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/approachcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit0@2x.png new file mode 100644 index 0000000000..bdb2bcbc41 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100@2x.png new file mode 100644 index 0000000000..7db8eb3124 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100k@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100k@2x.png new file mode 100644 index 0000000000..206840e467 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100k@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300@2x.png new file mode 100644 index 0000000000..2c7c07852f Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300g@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300g@2x.png new file mode 100644 index 0000000000..1ce746e3a4 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300g@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300k@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300k@2x.png new file mode 100755 index 0000000000..b0db9c00af Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300k@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit50@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit50@2x.png new file mode 100644 index 0000000000..94c09d263a Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit50@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircle@2x.png new file mode 100755 index 0000000000..6674616472 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircleoverlay@2x.png new file mode 100755 index 0000000000..1f98c1697e Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircleoverlay@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-nd@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-nd@2x.png new file mode 100644 index 0000000000..626fd91e38 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-nd@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-spec@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-spec@2x.png new file mode 100644 index 0000000000..76fd9ab168 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-spec@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb0@2x.png new file mode 100644 index 0000000000..0a24a72808 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb1@2x.png new file mode 100644 index 0000000000..e99f076947 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb1@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb2@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb2@2x.png new file mode 100644 index 0000000000..cd36a0ae16 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb2@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb3@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb3@2x.png new file mode 100644 index 0000000000..f494bd3f51 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb3@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb4@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb4@2x.png new file mode 100644 index 0000000000..a5b19887d6 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb4@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb5@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb5@2x.png new file mode 100644 index 0000000000..4bb01f0e88 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb5@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb6@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb6@2x.png new file mode 100644 index 0000000000..859e0aa4c1 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb6@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb7@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb7@2x.png new file mode 100644 index 0000000000..90efda0994 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb7@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb8@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb8@2x.png new file mode 100644 index 0000000000..fcdf4ed4a4 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb8@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb9@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb9@2x.png new file mode 100644 index 0000000000..c990cf0fe6 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb9@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/approachcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/approachcircle@2x.png new file mode 100644 index 0000000000..72ef665478 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/approachcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit0@2x.png new file mode 100644 index 0000000000..a91072eb5b Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit100@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit100@2x.png new file mode 100644 index 0000000000..5eb202c021 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit100@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit300@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit300@2x.png new file mode 100644 index 0000000000..878c11cd67 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit300@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit50@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit50@2x.png new file mode 100644 index 0000000000..f64feded0c Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hit50@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircle@2x.png new file mode 100644 index 0000000000..043bfbfae1 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircleoverlay@2x.png new file mode 100644 index 0000000000..4233d9bb6e Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/hitcircleoverlay@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/approachcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/approachcircle@2x.png new file mode 100755 index 0000000000..0a6ec6535c Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/approachcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-0@2x.png new file mode 100644 index 0000000000..37e7e9143f Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-1@2x.png new file mode 100644 index 0000000000..b75c71927c Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit0-1@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-0@2x.png new file mode 100644 index 0000000000..7932667408 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-1@2x.png new file mode 100644 index 0000000000..0b0ae85972 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit100-1@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-0@2x.png new file mode 100644 index 0000000000..441c3ec21f Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-1@2x.png new file mode 100644 index 0000000000..910d8e2231 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit300-1@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-0@2x.png new file mode 100644 index 0000000000..6f92db28d3 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-0@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-1@2x.png new file mode 100644 index 0000000000..b28503e9f2 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hit50-1@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png new file mode 100755 index 0000000000..919d8f405c Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircle@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png new file mode 100755 index 0000000000..a9b2d95d88 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs new file mode 100644 index 0000000000..02716dc1d5 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Text.RegularExpressions; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public abstract class SkinnableTestScene : OsuGridTestScene + { + private Skin metricsSkin; + private Skin defaultSkin; + private Skin specialSkin; + + protected SkinnableTestScene() + : base(2, 2) + { + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + var dllStore = new DllResourceStore("osu.Game.Rulesets.Osu.Tests.dll"); + + metricsSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); + defaultSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/default_skin"), audio, false); + specialSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true); + } + + public void SetContents(Func creationFunction) + { + Cell(0).Child = new LocalSkinOverrideContainer(null) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction()); + Cell(1).Child = new LocalSkinOverrideContainer(metricsSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction()); + Cell(2).Child = new LocalSkinOverrideContainer(defaultSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction()); + Cell(3).Child = new LocalSkinOverrideContainer(specialSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction()); + } + + private class TestLegacySkin : LegacySkin + { + private readonly bool extrapolateAnimations; + + public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) + : base(skin, storage, audioManager, "skin.ini") + { + this.extrapolateAnimations = extrapolateAnimations; + } + + public override Texture GetTexture(string componentName) + { + // extrapolate frames to test longer animations + if (extrapolateAnimations) + { + var match = Regex.Match(componentName, "-([0-9]*)"); + + if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60) + return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}")); + } + + return base.GetTexture(componentName); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs new file mode 100644 index 0000000000..82a8d0e5e6 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneDrawableJudgement : SkinnableTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableJudgement), + typeof(DrawableOsuJudgement) + }; + + public TestSceneDrawableJudgement() + { + foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) + AddStep("Show " + result.GetDescription(), () => SetContents(() => + new DrawableOsuJudgement(new JudgementResult(null) { Type = result }, null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index d44a0cd841..84a7bfc53e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -7,7 +7,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Visual; using osuTK; using System.Collections.Generic; using System; @@ -19,37 +18,32 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneHitCircle : OsuTestScene + public class TestSceneHitCircle : SkinnableTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableHitCircle) }; - private readonly Container content; - protected override Container Content => content; - private int depthIndex; public TestSceneHitCircle() { - base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - - AddStep("Miss Big Single", () => testSingle(2)); - AddStep("Miss Medium Single", () => testSingle(5)); - AddStep("Miss Small Single", () => testSingle(7)); - AddStep("Hit Big Single", () => testSingle(2, true)); - AddStep("Hit Medium Single", () => testSingle(5, true)); - AddStep("Hit Small Single", () => testSingle(7, true)); - AddStep("Miss Big Stream", () => testStream(2)); - AddStep("Miss Medium Stream", () => testStream(5)); - AddStep("Miss Small Stream", () => testStream(7)); - AddStep("Hit Big Stream", () => testStream(2, true)); - AddStep("Hit Medium Stream", () => testStream(5, true)); - AddStep("Hit Small Stream", () => testStream(7, true)); + AddStep("Miss Big Single", () => SetContents(() => testSingle(2))); + AddStep("Miss Medium Single", () => SetContents(() => testSingle(5))); + AddStep("Miss Small Single", () => SetContents(() => testSingle(7))); + AddStep("Hit Big Single", () => SetContents(() => testSingle(2, true))); + AddStep("Hit Medium Single", () => SetContents(() => testSingle(5, true))); + AddStep("Hit Small Single", () => SetContents(() => testSingle(7, true))); + AddStep("Miss Big Stream", () => SetContents(() => testStream(2))); + AddStep("Miss Medium Stream", () => SetContents(() => testStream(5))); + AddStep("Miss Small Stream", () => SetContents(() => testStream(7))); + AddStep("Hit Big Stream", () => SetContents(() => testStream(2, true))); + AddStep("Hit Medium Stream", () => SetContents(() => testStream(5, true))); + AddStep("Hit Small Stream", () => SetContents(() => testStream(7, true))); } - private void testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) + private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) { positionOffset = positionOffset ?? Vector2.Zero; @@ -61,27 +55,33 @@ namespace osu.Game.Rulesets.Osu.Tests circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); - var drawable = new TestDrawableHitCircle(circle, auto) - { - Anchor = Anchor.Centre, - Depth = depthIndex++ - }; + var drawable = CreateDrawableHitCircle(circle, auto); foreach (var mod in Mods.Value.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); - Add(drawable); + return drawable; } - private void testStream(float circleSize, bool auto = false) + protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto) { + Anchor = Anchor.Centre, + Depth = depthIndex++ + }; + + private Drawable testStream(float circleSize, bool auto = false) + { + var container = new Container { RelativeSizeAxes = Axes.Both }; + Vector2 pos = new Vector2(-250, 0); for (int i = 0; i <= 1000; i += 100) { - testSingle(circleSize, auto, i, pos); + container.Add(testSingle(circleSize, auto, i, pos)); pos.X += 50; } + + return container; } protected class TestDrawableHitCircle : DrawableHitCircle diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index 3d8afd66f4..84a73c7cfc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs @@ -1,23 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; using osu.Framework.MathUtils; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneShaking : TestSceneHitCircle { - public override void Add(Drawable drawable) + protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) { - base.Add(drawable); + var drawableHitObject = base.CreateDrawableHitCircle(circle, auto); - if (drawable is TestDrawableHitCircle hitObject) - { - Scheduler.AddDelayed(() => hitObject.TriggerJudgement(), - hitObject.HitObject.StartTime - (hitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current); - } + Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), + drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current); + + return drawableHitObject; } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index c5a27205d6..29c71a8903 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; using osu.Game.Rulesets.Mods; @@ -27,83 +26,96 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneSlider : OsuTestScene + public class TestSceneSlider : SkinnableTestScene { public override IReadOnlyList RequiredTypes => new[] { + typeof(Slider), + typeof(SliderTick), + typeof(SliderTailCircle), typeof(SliderBall), typeof(SliderBody), - typeof(SliderTick), + typeof(SnakingSliderBody), typeof(DrawableSlider), typeof(DrawableSliderTick), + typeof(DrawableSliderTail), + typeof(DrawableSliderHead), typeof(DrawableRepeatPoint), typeof(DrawableOsuHitObject) }; - private readonly Container content; - protected override Container Content => content; + private Container content; + + protected override Container Content + { + get + { + if (content == null) + base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + + return content; + } + } private int depthIndex; public TestSceneSlider() { - base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + AddStep("Big Single", () => SetContents(() => testSimpleBig())); + AddStep("Medium Single", () => SetContents(() => testSimpleMedium())); + AddStep("Small Single", () => SetContents(() => testSimpleSmall())); + AddStep("Big 1 Repeat", () => SetContents(() => testSimpleBig(1))); + AddStep("Medium 1 Repeat", () => SetContents(() => testSimpleMedium(1))); + AddStep("Small 1 Repeat", () => SetContents(() => testSimpleSmall(1))); + AddStep("Big 2 Repeats", () => SetContents(() => testSimpleBig(2))); + AddStep("Medium 2 Repeats", () => SetContents(() => testSimpleMedium(2))); + AddStep("Small 2 Repeats", () => SetContents(() => testSimpleSmall(2))); - AddStep("Big Single", () => testSimpleBig()); - AddStep("Medium Single", () => testSimpleMedium()); - AddStep("Small Single", () => testSimpleSmall()); - AddStep("Big 1 Repeat", () => testSimpleBig(1)); - AddStep("Medium 1 Repeat", () => testSimpleMedium(1)); - AddStep("Small 1 Repeat", () => testSimpleSmall(1)); - AddStep("Big 2 Repeats", () => testSimpleBig(2)); - AddStep("Medium 2 Repeats", () => testSimpleMedium(2)); - AddStep("Small 2 Repeats", () => testSimpleSmall(2)); + AddStep("Slow Slider", () => SetContents(testSlowSpeed)); // slow long sliders take ages already so no repeat steps + AddStep("Slow Short Slider", () => SetContents(() => testShortSlowSpeed())); + AddStep("Slow Short Slider 1 Repeats", () => SetContents(() => testShortSlowSpeed(1))); + AddStep("Slow Short Slider 2 Repeats", () => SetContents(() => testShortSlowSpeed(2))); - AddStep("Slow Slider", testSlowSpeed); // slow long sliders take ages already so no repeat steps - AddStep("Slow Short Slider", () => testShortSlowSpeed()); - AddStep("Slow Short Slider 1 Repeats", () => testShortSlowSpeed(1)); - AddStep("Slow Short Slider 2 Repeats", () => testShortSlowSpeed(2)); + AddStep("Fast Slider", () => SetContents(() => testHighSpeed())); + AddStep("Fast Slider 1 Repeat", () => SetContents(() => testHighSpeed(1))); + AddStep("Fast Slider 2 Repeats", () => SetContents(() => testHighSpeed(2))); + AddStep("Fast Short Slider", () => SetContents(() => testShortHighSpeed())); + AddStep("Fast Short Slider 1 Repeat", () => SetContents(() => testShortHighSpeed(1))); + AddStep("Fast Short Slider 2 Repeats", () => SetContents(() => testShortHighSpeed(2))); + AddStep("Fast Short Slider 6 Repeats", () => SetContents(() => testShortHighSpeed(6))); - AddStep("Fast Slider", () => testHighSpeed()); - AddStep("Fast Slider 1 Repeat", () => testHighSpeed(1)); - AddStep("Fast Slider 2 Repeats", () => testHighSpeed(2)); - AddStep("Fast Short Slider", () => testShortHighSpeed()); - AddStep("Fast Short Slider 1 Repeat", () => testShortHighSpeed(1)); - AddStep("Fast Short Slider 2 Repeats", () => testShortHighSpeed(2)); - AddStep("Fast Short Slider 6 Repeats", () => testShortHighSpeed(6)); + AddStep("Perfect Curve", () => SetContents(() => testPerfect())); + AddStep("Perfect Curve 1 Repeat", () => SetContents(() => testPerfect(1))); + AddStep("Perfect Curve 2 Repeats", () => SetContents(() => testPerfect(2))); - AddStep("Perfect Curve", () => testPerfect()); - AddStep("Perfect Curve 1 Repeat", () => testPerfect(1)); - AddStep("Perfect Curve 2 Repeats", () => testPerfect(2)); + AddStep("Linear Slider", () => SetContents(() => testLinear())); + AddStep("Linear Slider 1 Repeat", () => SetContents(() => testLinear(1))); + AddStep("Linear Slider 2 Repeats", () => SetContents(() => testLinear(2))); - AddStep("Linear Slider", () => testLinear()); - AddStep("Linear Slider 1 Repeat", () => testLinear(1)); - AddStep("Linear Slider 2 Repeats", () => testLinear(2)); + AddStep("Bezier Slider", () => SetContents(() => testBezier())); + AddStep("Bezier Slider 1 Repeat", () => SetContents(() => testBezier(1))); + AddStep("Bezier Slider 2 Repeats", () => SetContents(() => testBezier(2))); - AddStep("Bezier Slider", () => testBezier()); - AddStep("Bezier Slider 1 Repeat", () => testBezier(1)); - AddStep("Bezier Slider 2 Repeats", () => testBezier(2)); + AddStep("Linear Overlapping", () => SetContents(() => testLinearOverlapping())); + AddStep("Linear Overlapping 1 Repeat", () => SetContents(() => testLinearOverlapping(1))); + AddStep("Linear Overlapping 2 Repeats", () => SetContents(() => testLinearOverlapping(2))); - AddStep("Linear Overlapping", () => testLinearOverlapping()); - AddStep("Linear Overlapping 1 Repeat", () => testLinearOverlapping(1)); - AddStep("Linear Overlapping 2 Repeats", () => testLinearOverlapping(2)); + AddStep("Catmull Slider", () => SetContents(() => testCatmull())); + AddStep("Catmull Slider 1 Repeat", () => SetContents(() => testCatmull(1))); + AddStep("Catmull Slider 2 Repeats", () => SetContents(() => testCatmull(2))); - AddStep("Catmull Slider", () => testCatmull()); - AddStep("Catmull Slider 1 Repeat", () => testCatmull(1)); - AddStep("Catmull Slider 2 Repeats", () => testCatmull(2)); + AddStep("Big Single, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset())); + AddStep("Big 1 Repeat, Large StackOffset", () => SetContents(() => testSimpleBigLargeStackOffset(1))); - AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset()); - AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1)); - - AddStep("Distance Overflow", () => testDistanceOverflow()); - AddStep("Distance Overflow 1 Repeat", () => testDistanceOverflow(1)); + AddStep("Distance Overflow", () => SetContents(() => testDistanceOverflow())); + AddStep("Distance Overflow 1 Repeat", () => SetContents(() => testDistanceOverflow(1))); } - private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); + private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); - private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10); + private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10); - private void testDistanceOverflow(int repeats = 0) + private Drawable testDistanceOverflow(int repeats = 0) { var slider = new Slider { @@ -120,22 +132,22 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = 10 }; - addSlider(slider, 2, 2); + return createDrawable(slider, 2, 2); } - private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); + private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); - private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); + private Drawable testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); - private void testSlowSpeed() => createSlider(speedMultiplier: 0.5); + private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5); - private void testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5); + private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5); - private void testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15); + private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15); - private void testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); + private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); - private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) + private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) { var slider = new Slider { @@ -151,10 +163,10 @@ namespace osu.Game.Rulesets.Osu.Tests StackHeight = stackHeight }; - addSlider(slider, circleSize, speedMultiplier); + return createDrawable(slider, circleSize, speedMultiplier); } - private void testPerfect(int repeats = 0) + private Drawable testPerfect(int repeats = 0) { var slider = new Slider { @@ -170,12 +182,12 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = createEmptySamples(repeats) }; - addSlider(slider, 2, 3); + return createDrawable(slider, 2, 3); } - private void testLinear(int repeats = 0) => createLinear(repeats); + private Drawable testLinear(int repeats = 0) => createLinear(repeats); - private void createLinear(int repeats) + private Drawable createLinear(int repeats) { var slider = new Slider { @@ -194,12 +206,12 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = createEmptySamples(repeats) }; - addSlider(slider, 2, 3); + return createDrawable(slider, 2, 3); } - private void testBezier(int repeats = 0) => createBezier(repeats); + private Drawable testBezier(int repeats = 0) => createBezier(repeats); - private void createBezier(int repeats) + private Drawable createBezier(int repeats) { var slider = new Slider { @@ -217,12 +229,12 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = createEmptySamples(repeats) }; - addSlider(slider, 2, 3); + return createDrawable(slider, 2, 3); } - private void testLinearOverlapping(int repeats = 0) => createOverlapping(repeats); + private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats); - private void createOverlapping(int repeats) + private Drawable createOverlapping(int repeats) { var slider = new Slider { @@ -241,12 +253,12 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = createEmptySamples(repeats) }; - addSlider(slider, 2, 3); + return createDrawable(slider, 2, 3); } - private void testCatmull(int repeats = 0) => createCatmull(repeats); + private Drawable testCatmull(int repeats = 0) => createCatmull(repeats); - private void createCatmull(int repeats = 0) + private Drawable createCatmull(int repeats = 0) { var repeatSamples = new List>(); for (int i = 0; i < repeats; i++) @@ -267,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Tests NodeSamples = repeatSamples }; - addSlider(slider, 3, 1); + return createDrawable(slider, 3, 1); } private List> createEmptySamples(int repeats) @@ -278,7 +290,7 @@ namespace osu.Game.Rulesets.Osu.Tests return repeatSamples; } - private void addSlider(Slider slider, float circleSize, double speedMultiplier) + private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier) { var cpi = new ControlPointInfo(); cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); @@ -296,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests drawable.OnNewResult += onNewResult; - Add(drawable); + return drawable; } private float judgementOffsetDirection = 1; diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index bb3e5a66f3..197309c7c4 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 @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index 7bb1f42802..bb19b783aa 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osuTK; namespace osu.Game.Rulesets.Osu.Beatmaps { @@ -208,17 +209,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime) break; + // The start position of the hitobject, or the position at the end of the path if the hitobject is a slider + Vector2 position2 = currHitObject is Slider currSlider + ? currSlider.Position + currSlider.Path.PositionAt(1) + : currHitObject.Position; + if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance) { currHitObject.StackHeight++; - startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime; + startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[j].StartTime; } - else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance) + else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) { //Case for sliders - bump notes down and right, rather than up and left. sliderStack++; beatmap.HitObjects[j].StackHeight -= sliderStack; - startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime; + startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[j].StartTime; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs index 7f6a60c400..fe11ead94d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components this.hitCircle = hitCircle; Origin = Anchor.Centre; - Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2); + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Scale = new Vector2(hitCircle.Scale); CornerRadius = Size.X / 2; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 957550a051..f1f55731b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components InternalChild = body = new ManualSliderBody { AccentColour = Color4.Transparent, - PathRadius = slider.Scale * 64 }; } @@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components body.BorderColour = colours.Yellow; PositionBindable.BindValueChanged(_ => updatePosition(), true); - ScaleBindable.BindValueChanged(scale => body.PathRadius = scale.NewValue * 64, true); + ScaleBindable.BindValueChanged(scale => body.PathRadius = scale.NewValue * OsuHitObject.OBJECT_RADIUS, true); } private void updatePosition() => Position = slider.StackedPosition; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 401bd28d7c..ca72f18e9c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -2,13 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.StateChanges; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModAutopilot : Mod + public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset { public override string Name => "Autopilot"; public override string Acronym => "AP"; @@ -17,5 +23,40 @@ namespace osu.Game.Rulesets.Osu.Mods 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 bool AllowFail => false; + + private OsuInputManager inputManager; + + private List replayFrames; + + private int currentFrame; + + public void Update(Playfield playfield) + { + if (currentFrame == replayFrames.Count - 1) return; + + double time = playfield.Time.Current; + + // Very naive implementation of autopilot based on proximity to replay frames. + // TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered). + if (Math.Abs(replayFrames[currentFrame + 1].Time - time) <= Math.Abs(replayFrames[currentFrame].Time - time)) + { + currentFrame++; + new MousePositionAbsoluteInput { Position = playfield.ToScreenSpace(replayFrames[currentFrame].Position) }.Apply(inputManager.CurrentState, inputManager); + } + + // TODO: Implement the functionality to automatically spin spinners + } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + // Grab the input manager to disable the user's cursor, and for future use + inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; + inputManager.AllowUserCursorMovement = false; + + // Generate the replay frames the cursor should follow + replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast().ToList(); + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index ddf708d0f1..2d940479f3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs new file mode 100644 index 0000000000..62b5ecfd58 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModSpinIn : Mod, IApplicableToDrawableHitObjects, IReadFromConfig + { + public override string Name => "Spin In"; + public override string Acronym => "SI"; + public override IconUsage Icon => FontAwesome.Solid.Undo; + public override ModType Type => ModType.Fun; + public override string Description => "Circles spin in. No approach circles."; + public override double ScoreMultiplier => 1; + + // todo: this mod should be able to be compatible with hidden with a bit of further implementation. + public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden) }; + + private const int rotate_offset = 360; + private const float rotate_starting_width = 2; + + private Bindable increaseFirstObjectVisibility = new Bindable(); + + public void ReadFromConfig(OsuConfigManager config) + { + increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + } + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) + { + switch (drawable) + { + case DrawableSpinner _: + continue; + + default: + drawable.ApplyCustomUpdateState += applyZoomState; + break; + } + } + } + + private void applyZoomState(DrawableHitObject drawable, ArmedState state) + { + var h = (OsuHitObject)drawable.HitObject; + + switch (drawable) + { + case DrawableHitCircle circle: + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + { + circle.ApproachCircle.Hide(); + + circle.RotateTo(rotate_offset).Then().RotateTo(0, h.TimePreempt, Easing.InOutSine); + circle.ScaleTo(new Vector2(rotate_starting_width, 0)).Then().ScaleTo(1, h.TimePreempt, Easing.InOutSine); + + // bypass fade in. + if (state == ArmedState.Idle) + circle.FadeIn(); + } + + break; + + case DrawableSlider slider: + using (slider.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + { + slider.ScaleTo(0).Then().ScaleTo(1, h.TimePreempt, Easing.InOutSine); + + // bypass fade in. + if (state == ArmedState.Idle) + slider.FadeIn(); + } + + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs index 571756d056..f0db548e74 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "TD"; public override double ScoreMultiplier => 1; + public override ModType Type => ModType.System; + public override bool Ranked => true; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs index ad6a15718a..e926ade41b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods private Bindable increaseFirstObjectVisibility = new Bindable(); + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; + public void ReadFromConfig(OsuConfigManager config) { increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); @@ -64,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSlider _: case DrawableHitCircle _: { - using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); break; } @@ -75,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Mods { case DrawableHitCircle circle: // we don't want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) circle.ApproachCircle.Hide(); break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index aacf3ee08d..523e911434 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -36,12 +36,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Child = new Box { Size = new Vector2(width), - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0.5f, } - }, restrictSize: false); + }, confineMode: ConfineMode.NoScaling); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 7569626230..a269b87c75 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -97,13 +97,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Position = pointStartPosition, Rotation = rotation, Alpha = 0, - Scale = new Vector2(1.5f), + Scale = new Vector2(1.5f * currHitObject.Scale), }); using (fp.BeginAbsoluteSequence(fadeInTime)) { fp.FadeIn(currHitObject.TimeFadeIn); - fp.ScaleTo(1, currHitObject.TimeFadeIn, Easing.Out); + fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index fef0bfdc2c..0af278f6a4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -6,34 +6,31 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osuTK; using osu.Game.Rulesets.Scoring; -using osuTK.Graphics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach { public ApproachCircle ApproachCircle; - private readonly CirclePiece circle; - private readonly RingPiece ring; - private readonly FlashPiece flash; - private readonly ExplodePiece explode; - private readonly NumberPiece number; - private readonly GlowPiece glow; private readonly IBindable positionBindable = new Bindable(); private readonly IBindable stackHeightBindable = new Bindable(); private readonly IBindable scaleBindable = new Bindable(); - public OsuAction? HitAction => circle.HitAction; - - private readonly Container explodeContainer; + public OsuAction? HitAction => hitArea.HitAction; private readonly Container scaleContainer; + private readonly HitArea hitArea; + + private readonly SkinnableDrawable mainContent; + public DrawableHitCircle(HitCircle h) : base(h) { @@ -48,44 +45,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - Child = explodeContainer = new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Children = new Drawable[] + hitArea = new HitArea { - glow = new GlowPiece(), - circle = new CirclePiece + Hit = () => { - Hit = () => - { - if (AllJudged) - return false; + if (AllJudged) + return false; - UpdateResult(true); - return true; - }, + UpdateResult(true); + return true; }, - number = new NumberPiece - { - Text = (HitObject.IndexInCurrentCombo + 1).ToString(), - }, - ring = new RingPiece(), - flash = new FlashPiece(), - explode = new ExplodePiece(), - ApproachCircle = new ApproachCircle - { - Alpha = 0, - Scale = new Vector2(4), - } + }, + mainContent = new SkinnableDrawable("Play/osu/hitcircle", _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)), + ApproachCircle = new ApproachCircle + { + Alpha = 0, + Scale = new Vector2(4), } } }, }; - //may not be so correct - Size = circle.DrawSize; + Size = hitArea.DrawSize; } [BackgroundDependencyLoader] @@ -98,19 +81,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables positionBindable.BindTo(HitObject.PositionBindable); stackHeightBindable.BindTo(HitObject.StackHeightBindable); scaleBindable.BindTo(HitObject.ScaleBindable); - } - public override Color4 AccentColour - { - get => base.AccentColour; - set - { - base.AccentColour = value; - explode.Colour = AccentColour; - glow.Colour = AccentColour; - circle.Colour = AccentColour; - ApproachCircle.Colour = AccentColour; - } + AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -134,19 +106,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApplyResult(r => r.Type = result); } - protected override void UpdatePreemptState() + protected override void UpdateInitialTransforms() { - base.UpdatePreemptState(); + base.UpdateInitialTransforms(); + + mainContent.FadeInFromZero(HitObject.TimeFadeIn); ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt)); - ApproachCircle.ScaleTo(1.1f, HitObject.TimePreempt); + ApproachCircle.ScaleTo(1f, HitObject.TimePreempt); ApproachCircle.Expire(true); } - protected override void UpdateCurrentState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { - glow.FadeOut(400); - switch (state) { case ArmedState.Idle: @@ -154,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Expire(true); - circle.HitAction = null; + hitArea.HitAction = null; // override lifetime end as FadeIn may have been changed externally, causing out expiration to be too early. LifetimeEnd = HitObject.StartTime + HitObject.HitWindows.HalfWindowFor(HitResult.Miss); @@ -169,29 +141,50 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Hit: ApproachCircle.FadeOut(50); - const double flash_in = 40; - flash.FadeTo(0.8f, flash_in) - .Then() - .FadeOut(100); - - explode.FadeIn(flash_in); - - using (BeginDelayedSequence(flash_in, true)) - { - //after the flash, we can hide some elements that were behind it - ring.FadeOut(); - circle.FadeOut(); - number.FadeOut(); - - this.FadeOut(800); - explodeContainer.ScaleTo(1.5f, 400, Easing.OutQuad); - } - - Expire(); + // todo: temporary / arbitrary + this.Delay(800).Expire(); break; } } public Drawable ProxiedLayer => ApproachCircle; + + private class HitArea : Drawable, IKeyBindingHandler + { + // IsHovered is used + public override bool HandlePositionalInput => true; + + public Func Hit; + + public OsuAction? HitAction; + + public HitArea() + { + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + public bool OnPressed(OsuAction action) + { + switch (action) + { + case OsuAction.LeftButton: + case OsuAction.RightButton: + if (IsHovered && (Hit?.Invoke() ?? false)) + { + HitAction = action; + return true; + } + + break; + } + + return false; + } + + public bool OnReleased(OsuAction action) => false; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index f372cb65ce..b4f5642f45 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -1,15 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Scoring; -using osu.Game.Skinning; -using osuTK.Graphics; using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -29,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ShakeDuration = 30, RelativeSizeAxes = Axes.Both }); + Alpha = 0; } @@ -38,47 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren); protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable); - protected sealed override void UpdateState(ArmedState state) - { - double transformTime = HitObject.StartTime - HitObject.TimePreempt; - - base.ApplyTransformsAt(transformTime, true); - base.ClearTransformsAfter(transformTime, true); - - using (BeginAbsoluteSequence(transformTime, true)) - { - UpdatePreemptState(); - - var judgementOffset = Math.Min(HitObject.HitWindows.HalfWindowFor(HitResult.Miss), Result?.TimeOffset ?? 0); - - using (BeginDelayedSequence(HitObject.TimePreempt + judgementOffset, true)) - UpdateCurrentState(state); - } - } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - - if (HitObject is IHasComboInformation combo) - AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; - } - - protected virtual void UpdatePreemptState() => this.FadeIn(HitObject.TimeFadeIn); - - protected virtual void UpdateCurrentState(ArmedState state) - { - } - - // Todo: At some point we need to move these to DrawableHitObject after ensuring that all other Rulesets apply - // transforms in the same way and don't rely on them not being cleared - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) - { - } - - public override void ApplyTransformsAt(double time, bool propagateChildren = false) - { - } + protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; private OsuInputManager osuActionInputManager; internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index cce6dfe106..1db1eec33e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; @@ -20,34 +22,47 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private double animDuration; + private readonly SkinnableDrawable scaleContainer; + public DrawableRepeatPoint(RepeatPoint repeatPoint, DrawableSlider drawableSlider) : base(repeatPoint) { this.repeatPoint = repeatPoint; this.drawableSlider = drawableSlider; - Size = new Vector2(45 * repeatPoint.Scale); + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; Origin = Anchor.Centre; - InternalChildren = new Drawable[] + InternalChild = scaleContainer = new SkinnableDrawable("Play/osu/reversearrow", _ => new SpriteIcon { - new SkinnableDrawable("Play/osu/reversearrow", _ => new SpriteIcon - { - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.ChevronRight - }, restrictSize: false) + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(0.35f) + }, confineMode: ConfineMode.NoScaling) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }; } + private readonly IBindable scaleBindable = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + scaleBindable.BindTo(HitObject.ScaleBindable); + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (repeatPoint.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); } - protected override void UpdatePreemptState() + protected override void UpdateInitialTransforms() { animDuration = Math.Min(150, repeatPoint.SpanDuration / 2); @@ -57,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ); } - protected override void UpdateCurrentState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { switch (state) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 05cb42d853..1749ea1f60 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -48,10 +48,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { - Body = new SnakingSliderBody(s) - { - PathRadius = s.Scale * 64, - }, + Body = new SnakingSliderBody(s), ticks = new Container { RelativeSizeAxes = Axes.Both }, repeatPoints = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) @@ -96,6 +93,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + Body.FadeInFromZero(HitObject.TimeFadeIn); + } + [BackgroundDependencyLoader] private void load() { @@ -105,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); scaleBindable.BindValueChanged(scale => { - Body.PathRadius = scale.NewValue * 64; + updatePathRadius(); Ball.Scale = new Vector2(scale.NewValue); }); @@ -114,20 +118,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathBindable.BindTo(slider.PathBindable); pathBindable.BindValueChanged(_ => Body.Refresh()); - } - public override Color4 AccentColour - { - get => base.AccentColour; - set + AccentColour.BindValueChanged(colour => { - base.AccentColour = value; - Body.AccentColour = AccentColour; - Ball.AccentColour = AccentColour; + Body.AccentColour = colour.NewValue; foreach (var drawableHitObject in NestedHitObjects) - drawableHitObject.AccentColour = AccentColour; - } + drawableHitObject.AccentColour.Value = colour.NewValue; + }, true); } public readonly Bindable Tracking = new Bindable(); @@ -156,16 +154,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + public override void OnKilled() + { + base.OnKilled(); + Body.RecyclePath(); + } + + private float sliderPathRadius; + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); Body.BorderSize = skin.GetValue(s => s.SliderBorderSize) ?? SliderBody.DEFAULT_BORDER_SIZE; - Body.AccentColour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderTrackOverride") ? s.CustomColours["SliderTrackOverride"] : (Color4?)null) ?? AccentColour; + sliderPathRadius = skin.GetValue(s => s.SliderPathRadius) ?? OsuHitObject.OBJECT_RADIUS; + updatePathRadius(); + + Body.AccentColour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderTrackOverride") ? s.CustomColours["SliderTrackOverride"] : (Color4?)null) ?? AccentColour.Value; Body.BorderColour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBorder") ? s.CustomColours["SliderBorder"] : (Color4?)null) ?? Color4.White; - Ball.AccentColour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? AccentColour; } + private void updatePathRadius() => Body.PathRadius = slider.Scale * sliderPathRadius; + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (userTriggered || Time.Current < slider.EndTime) @@ -189,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } - protected override void UpdateCurrentState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { Ball.FadeIn(); Ball.ScaleTo(HitObject.Scale); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 72b648bfd0..653e73ac3f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osuTK; @@ -16,51 +18,62 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public const double ANIM_DURATION = 150; + private const float default_tick_size = 16; + public bool Tracking { get; set; } public override bool DisplayResult => false; + private readonly SkinnableDrawable scaleContainer; + public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { - Size = new Vector2(16) * sliderTick.Scale; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Origin = Anchor.Centre; - InternalChildren = new Drawable[] + InternalChild = scaleContainer = new SkinnableDrawable("Play/osu/sliderscorepoint", _ => new CircularContainer { - new SkinnableDrawable("Play/osu/sliderscorepoint", _ => new Container + Masking = true, + Origin = Anchor.Centre, + Size = new Vector2(default_tick_size), + BorderThickness = default_tick_size / 4, + BorderColour = Color4.White, + Child = new Box { - Masking = true, RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - CornerRadius = Size.X / 2, - - BorderThickness = 2, - BorderColour = Color4.White, - - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = AccentColour, - Alpha = 0.3f, - } - }, restrictSize: false) + Colour = AccentColour.Value, + Alpha = 0.3f, + } + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }; } + private readonly IBindable scaleBindable = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + scaleBindable.BindTo(HitObject.ScaleBindable); + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); } - protected override void UpdatePreemptState() + protected override void UpdateInitialTransforms() { this.FadeOut().FadeIn(ANIM_DURATION); this.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf); } - protected override void UpdateCurrentState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { switch (state) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1794da54b7..a0bd301fdb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -196,9 +196,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint); } - protected override void UpdatePreemptState() + protected override void UpdateInitialTransforms() { - base.UpdatePreemptState(); + base.UpdateInitialTransforms(); circleContainer.ScaleTo(Spinner.Scale * 0.3f); circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint); @@ -213,7 +213,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables .ScaleTo(1, 500, Easing.OutQuint); } - protected override void UpdateCurrentState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { var sequence = this.Delay(Spinner.Duration).FadeOut(160); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs index 9981585f9e..5813197336 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -24,7 +25,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces [BackgroundDependencyLoader] private void load(TextureStore textures) { - Child = new SkinnableSprite("Play/osu/approachcircle"); + Child = new SkinnableApproachCircle(); + } + + private class SkinnableApproachCircle : SkinnableSprite + { + public SkinnableApproachCircle() + : base("Play/osu/approachcircle") + { + } + + protected override Drawable CreateDefault(string name) + { + var drawable = base.CreateDefault(name); + + // account for the sprite being used for the default approach circle being taken from stable, + // when hitcircles have 5px padding on each size. this should be removed if we update the sprite. + drawable.Scale = new Vector2(128 / 118f); + + return drawable; + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 786cac7198..a59cfc1123 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -1,54 +1,45 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Bindings; -using osu.Game.Skinning; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class CirclePiece : Container, IKeyBindingHandler + public class CirclePiece : CompositeDrawable { - // IsHovered is used - public override bool HandlePositionalInput => true; - - public Func Hit; - - public OsuAction? HitAction; - public CirclePiece() { - Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2); + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Masking = true; CornerRadius = Size.X / 2; Anchor = Anchor.Centre; Origin = Anchor.Centre; - - InternalChild = new SkinnableDrawable("Play/osu/hitcircle", _ => new DefaultCirclePiece()); } - public bool OnPressed(OsuAction action) + [BackgroundDependencyLoader] + private void load(TextureStore textures) { - switch (action) + InternalChildren = new Drawable[] { - case OsuAction.LeftButton: - case OsuAction.RightButton: - if (IsHovered && (Hit?.Invoke() ?? false)) - { - HitAction = action; - return true; - } - - break; - } - - return false; + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(@"Play/osu/disc"), + }, + new TrianglesPiece + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Alpha = 0.5f, + } + }; } - - public bool OnReleased(OsuAction action) => false; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs deleted file mode 100644 index 047ff943ff..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces -{ - public class DefaultCirclePiece : Container - { - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - RelativeSizeAxes = Axes.Both; - Children = new Drawable[] - { - new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = textures.Get(@"Play/osu/disc"), - }, - new TrianglesPiece - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Alpha = 0.5f, - } - }; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs index b960f40578..1d21347cba 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs @@ -12,17 +12,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public ExplodePiece() { - Size = new Vector2(128); + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Anchor = Anchor.Centre; Origin = Anchor.Centre; - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; Alpha = 0; Child = new SkinnableDrawable("Play/osu/hitcircle-explode", _ => new TrianglesPiece { - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, Alpha = 0.2f, }, s => s.GetTexture("Play/osu/hitcircle") == null); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs index 8e5eb886aa..1e3af567fe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; @@ -13,12 +13,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public FlashPiece() { - Size = new Vector2(128); + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Anchor = Anchor.Centre; Origin = Anchor.Centre; - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; Alpha = 0; Child = new SkinnableDrawable("Play/osu/hitcircle-flash", name => new CircularContainer diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs index 917695c790..a36d9e96c8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = textures.Get(name), - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0.5f }, s => s.GetTexture("Play/osu/hitcircle") == null); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs new file mode 100644 index 0000000000..944c93bb6d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public class MainCirclePiece : CompositeDrawable + { + private readonly CirclePiece circle; + private readonly RingPiece ring; + private readonly FlashPiece flash; + private readonly ExplodePiece explode; + private readonly NumberPiece number; + private readonly GlowPiece glow; + + public MainCirclePiece(int index) + { + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + glow = new GlowPiece(), + circle = new CirclePiece(), + number = new NumberPiece + { + Text = (index + 1).ToString(), + }, + ring = new RingPiece(), + flash = new FlashPiece(), + explode = new ExplodePiece(), + }; + } + + private readonly IBindable state = new Bindable(); + + private readonly Bindable accentColour = new Bindable(); + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + state.BindTo(drawableObject.State); + state.BindValueChanged(updateState, true); + + accentColour.BindTo(drawableObject.AccentColour); + accentColour.BindValueChanged(colour => + { + explode.Colour = colour.NewValue; + glow.Colour = colour.NewValue; + circle.Colour = colour.NewValue; + }, true); + } + + private void updateState(ValueChangedEvent state) + { + glow.FadeOut(400); + + switch (state.NewValue) + { + case ArmedState.Hit: + const double flash_in = 40; + const double flash_out = 100; + + flash.FadeTo(0.8f, flash_in) + .Then() + .FadeOut(flash_out); + + explode.FadeIn(flash_in); + this.ScaleTo(1.5f, 400, Easing.OutQuad); + + using (BeginDelayedSequence(flash_in, true)) + { + //after the flash, we can hide some elements that were behind it + ring.FadeOut(); + circle.FadeOut(); + number.FadeOut(); + + this.FadeOut(800); + } + + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index 84034d3ee9..e8dc63abca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, - }, restrictSize: false) + }, confineMode: ConfineMode.NoScaling) { Text = @"1" } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index 28180a7f71..575f2c92c5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public RingPiece() { - Size = new Vector2(128); + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 7d1d77ae96..329aed7b81 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -3,11 +3,13 @@ using System; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK.Graphics; using osu.Game.Skinning; @@ -17,90 +19,43 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition { - private const float width = 128; - - private Color4 accentColour = Color4.Black; - public Func GetInitialHitAction; - /// - /// The colour that is used for the slider ball. - /// - public Color4 AccentColour - { - get => accentColour; - set - { - accentColour = value; - if (drawableBall != null) - drawableBall.Colour = value; - } - } - private readonly Slider slider; public readonly Drawable FollowCircle; - private Drawable drawableBall; private readonly DrawableSlider drawableSlider; public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { this.drawableSlider = drawableSlider; this.slider = slider; - Masking = true; - AutoSizeAxes = Axes.Both; - Blending = BlendingMode.Additive; + + Blending = BlendingParameters.Additive; Origin = Anchor.Centre; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + Children = new[] { - FollowCircle = new Container + FollowCircle = new FollowCircleContainer { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Width = width, - Height = width, + RelativeSizeAxes = Axes.Both, Alpha = 0, - Child = new SkinnableDrawable("Play/osu/sliderfollowcircle", _ => new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = 5, - BorderColour = Color4.Orange, - Blending = BlendingMode.Additive, - Child = new Box - { - Colour = Color4.Orange, - RelativeSizeAxes = Axes.Both, - Alpha = 0.2f, - } - }), + Child = new SkinnableDrawable("Play/osu/sliderfollowcircle", _ => new DefaultFollowCircle()), }, new CircularContainer { Masking = true, - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 1, Child = new Container { - Width = width, - Height = width, - // TODO: support skin filename animation (sliderb0, sliderb1...) - Child = new SkinnableDrawable("Play/osu/sliderb", _ => new CircularContainer - { - Masking = true, - RelativeSizeAxes = Axes.Both, - BorderThickness = 10, - BorderColour = Color4.White, - Alpha = 1, - Child = drawableBall = new Box - { - Colour = AccentColour, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - } - }), + RelativeSizeAxes = Axes.Both, + Child = new SkinnableDrawable("Play/osu/sliderball", _ => new DefaultSliderBall()), } } }; @@ -193,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // in valid time range Time.Current >= slider.StartTime && Time.Current < slider.EndTime && // in valid position range - lastScreenSpaceMousePosition.HasValue && base.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && + lastScreenSpaceMousePosition.HasValue && FollowCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action (actions?.Any(isValidTrackingAction) ?? false); } @@ -212,9 +167,77 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return action == OsuAction.LeftButton || action == OsuAction.RightButton; } + private Vector2? lastPosition; + public void UpdateProgress(double completionProgress) { - Position = slider.CurvePositionAt(completionProgress); + var newPos = slider.CurvePositionAt(completionProgress); + + var diff = lastPosition.HasValue ? lastPosition.Value - newPos : newPos - slider.CurvePositionAt(completionProgress + 0.01f); + if (diff == Vector2.Zero) + return; + + Position = newPos; + Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); + + lastPosition = newPos; + } + + private class FollowCircleContainer : Container + { + public override bool HandlePositionalInput => true; + } + + public class DefaultFollowCircle : CompositeDrawable + { + public DefaultFollowCircle() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 5, + BorderColour = Color4.Orange, + Blending = BlendingParameters.Additive, + Child = new Box + { + Colour = Color4.Orange, + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + } + }; + } + } + + public class DefaultSliderBall : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject, ISkinSource skin) + { + RelativeSizeAxes = Axes.Both; + + float radius = skin.GetValue(s => s.SliderPathRadius) ?? OsuHitObject.OBJECT_RADIUS; + + InternalChild = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BorderThickness = 10, + BorderColour = Color4.White, + Alpha = 1, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Alpha = 0.4f, + } + }; + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 33b3667c4f..24a437c20e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; @@ -13,10 +14,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public const float DEFAULT_BORDER_SIZE = 1; - private readonly SliderPath path; + private SliderPath path; + protected Path Path => path; - public float PathRadius + public virtual float PathRadius { get => path.PathRadius; set => path.PathRadius = value; @@ -74,7 +76,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected SliderBody() { - InternalChild = path = new SliderPath(); + RecyclePath(); + } + + /// + /// Initialises a new , releasing all resources retained by the old one. + /// + public virtual void RecyclePath() + { + InternalChild = path = new SliderPath + { + Position = path?.Position ?? Vector2.Zero, + PathRadius = path?.PathRadius ?? 10, + AccentColour = path?.AccentColour ?? Color4.White, + BorderColour = path?.BorderColour ?? Color4.White, + BorderSize = path?.BorderSize ?? DEFAULT_BORDER_SIZE, + Vertices = path?.Vertices ?? Array.Empty() + }; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => path.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index 73b184bffe..70a1bad4a3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -23,6 +24,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public double? SnakedStart { get; private set; } public double? SnakedEnd { get; private set; } + public override float PathRadius + { + get => base.PathRadius; + set + { + if (base.PathRadius == value) + return; + + base.PathRadius = value; + + Refresh(); + } + } + public override Vector2 PathOffset => snakedPathOffset; /// @@ -54,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var spanProgress = slider.ProgressAt(completionProgress); double start = 0; - double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadeIn, 0, 1) : 1; + double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1; if (span >= slider.SpanCount() - 1) { @@ -78,9 +93,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces slider.Path.GetPathToProgress(CurrentCurve, 0, 1); SetVertices(CurrentCurve); - // The body is sized to the full path size to avoid excessive autosize computations + // Force the body to be the final path size to avoid excessive autosize computations + Path.AutoSizeAxes = Axes.Both; Size = Path.Size; + updatePathSize(); + snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); @@ -93,6 +111,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces setRange(lastSnakedStart, lastSnakedEnd); } + public override void RecyclePath() + { + base.RecyclePath(); + updatePathSize(); + } + + private void updatePathSize() + { + // Force the path to its final size to avoid excessive framebuffer resizes + Path.AutoSizeAxes = Axes.None; + Path.Size = Size; + } + private void setRange(double p0, double p1) { if (p0 > p1) diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 364c182dd4..b52bfcd181 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition { - public const double OBJECT_RADIUS = 64; + public const float OBJECT_RADIUS = 64; public double TimePreempt = 600; public double TimeFadeIn = 400; @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); - TimeFadeIn = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1200, 800, 300); + TimeFadeIn = 400; // as per osu-stable Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index a4638c31f2..d3279652c7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; public double Duration => EndTime - StartTime; - private Cached endPositionCache; + private readonly Cached endPositionCache = new Cached(); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index b9e083d35b..cdea7276f3 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -18,6 +18,12 @@ namespace osu.Game.Rulesets.Osu set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value; } + /// + /// Whether the user's cursor movement events should be accepted. + /// Can be used to block only movement while still accepting button input. + /// + public bool AllowUserCursorMovement { get; set; } = true; + protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new OsuKeyBindingContainer(ruleset, variant, unique); @@ -26,6 +32,13 @@ namespace osu.Game.Rulesets.Osu { } + protected override bool Handle(UIEvent e) + { + if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; + + return base.Handle(e); + } + private class OsuKeyBindingContainer : RulesetKeyBindingContainer { public bool AllowUserPresses = true; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index baa4aff413..d50d4f401c 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -14,7 +14,6 @@ using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; @@ -134,8 +133,15 @@ namespace osu.Game.Rulesets.Osu { new OsuModTransform(), new OsuModWiggle(), + new OsuModSpinIn(), new MultiMod(new OsuModGrow(), new OsuModDeflate()), - new MultiMod(new ModWindUp(), new ModWindDown()), + new MultiMod(new ModWindUp(), new ModWindDown()), + }; + + case ModType.System: + return new Mod[] + { + new OsuModTouchDevice(), }; default: diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json new file mode 100644 index 0000000000..b994cbd85a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking-expected-conversion.json @@ -0,0 +1,278 @@ +{ + "Mappings": [{ + "StartTime": 32165, + "Objects": [{ + "StartTime": 32165, + "EndTime": 32165, + "X": 32, + "Y": 320 + }] + }, + { + "StartTime": 32517, + "Objects": [{ + "StartTime": 32517, + "EndTime": 32517, + "X": 246.396057, + "Y": 182.396057 + }] + }, + { + "StartTime": 32605, + "Objects": [{ + "StartTime": 32605, + "EndTime": 32605, + "X": 249.597382, + "Y": 185.597382 + }] + }, + { + "StartTime": 32693, + "Objects": [{ + "StartTime": 32693, + "EndTime": 32693, + "X": 252.798691, + "Y": 188.798691 + }] + }, + { + "StartTime": 32781, + "Objects": [{ + "StartTime": 32781, + "EndTime": 32781, + "X": 256, + "Y": 192 + }] + }, + { + "StartTime": 33248, + "Objects": [{ + "StartTime": 33248, + "EndTime": 33248, + "X": 39.3960648, + "Y": 76.3960648 + }] + }, + { + "StartTime": 33307, + "Objects": [{ + "StartTime": 33307, + "EndTime": 33307, + "X": 42.5973778, + "Y": 79.597374 + }] + }, + { + "StartTime": 33383, + "Objects": [{ + "StartTime": 33383, + "EndTime": 33383, + "X": 45.798687, + "Y": 82.79869 + }] + }, + { + "StartTime": 33459, + "Objects": [{ + "StartTime": 33459, + "EndTime": 33459, + "X": 49, + "Y": 86 + }, + { + "StartTime": 33635, + "EndTime": 33635, + "X": 123.847847, + "Y": 85.7988 + }, + { + "StartTime": 33811, + "EndTime": 33811, + "X": 198.6957, + "Y": 85.5975952 + }, + { + "StartTime": 33988, + "EndTime": 33988, + "X": 273.9688, + "Y": 85.39525 + }, + { + "StartTime": 34164, + "EndTime": 34164, + "X": 348.816681, + "Y": 85.19404 + }, + { + "StartTime": 34246, + "EndTime": 34246, + "X": 398.998718, + "Y": 85.05914 + } + ] + }, + { + "StartTime": 34341, + "Objects": [{ + "StartTime": 34341, + "EndTime": 34341, + "X": 401.201324, + "Y": 88.20131 + }] + }, + { + "StartTime": 34400, + "Objects": [{ + "StartTime": 34400, + "EndTime": 34400, + "X": 404.402618, + "Y": 91.402626 + }] + }, + { + "StartTime": 34459, + "Objects": [{ + "StartTime": 34459, + "EndTime": 34459, + "X": 407.603943, + "Y": 94.6039352 + }] + }, + { + "StartTime": 34989, + "Objects": [{ + "StartTime": 34989, + "EndTime": 34989, + "X": 163, + "Y": 138 + }, + { + "StartTime": 35018, + "EndTime": 35018, + "X": 188, + "Y": 138 + } + ] + }, + { + "StartTime": 35106, + "Objects": [{ + "StartTime": 35106, + "EndTime": 35106, + "X": 163, + "Y": 138 + }, + { + "StartTime": 35135, + "EndTime": 35135, + "X": 188, + "Y": 138 + } + ] + }, + { + "StartTime": 35224, + "Objects": [{ + "StartTime": 35224, + "EndTime": 35224, + "X": 163, + "Y": 138 + }, + { + "StartTime": 35253, + "EndTime": 35253, + "X": 188, + "Y": 138 + } + ] + }, + { + "StartTime": 35695, + "Objects": [{ + "StartTime": 35695, + "EndTime": 35695, + "X": 166, + "Y": 76 + }, + { + "StartTime": 35871, + "EndTime": 35871, + "X": 240.99855, + "Y": 75.53417 + }, + { + "StartTime": 36011, + "EndTime": 36011, + "X": 315.9971, + "Y": 75.0683441 + } + ] + }, + { + "StartTime": 36106, + "Objects": [{ + "StartTime": 36106, + "EndTime": 36106, + "X": 315, + "Y": 75 + }, + { + "StartTime": 36282, + "EndTime": 36282, + "X": 240.001526, + "Y": 75.47769 + }, + { + "StartTime": 36422, + "EndTime": 36422, + "X": 165.003052, + "Y": 75.95539 + } + ] + }, + { + "StartTime": 36518, + "Objects": [{ + "StartTime": 36518, + "EndTime": 36518, + "X": 166, + "Y": 76 + }, + { + "StartTime": 36694, + "EndTime": 36694, + "X": 240.99855, + "Y": 75.53417 + }, + { + "StartTime": 36834, + "EndTime": 36834, + "X": 315.9971, + "Y": 75.0683441 + } + ] + }, + { + "StartTime": 36929, + "Objects": [{ + "StartTime": 36929, + "EndTime": 36929, + "X": 315, + "Y": 75 + }, + { + "StartTime": 37105, + "EndTime": 37105, + "X": 240.001526, + "Y": 75.47769 + }, + { + "StartTime": 37245, + "EndTime": 37245, + "X": 165.003052, + "Y": 75.95539 + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking.osu new file mode 100644 index 0000000000..4bc9226d67 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/old-stacking.osu @@ -0,0 +1,40 @@ +osu file format v3 + +[Difficulty] +HPDrainRate:3 +CircleSize:5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:1.5 +SliderTickRate:2 + +[TimingPoints] +48,352.941176470588,4,1,1,100,1,0 + +[HitObjects] +// Hit circles +32,320,32165,5,2,0:0:0:0: +256,192,32517,5,0,0:0:0:0: +256,192,32605,1,0,0:0:0:0: +256,192,32693,1,0,0:0:0:0: +256,192,32781,1,0,0:0:0:0: + +// Hit circles on slider endpoints +49,86,33248,1,0,0:0:0:0: +49,86,33307,1,0,0:0:0:0: +49,86,33383,1,0,0:0:0:0: +49,86,33459,2,0,L|421:85,1,350 +398,85,34341,1,0,0:0:0:0: +398,85,34400,1,0,0:0:0:0: +398,85,34459,1,0,0:0:0:0: + +// Sliders +163,138,34989,2,0,L|196:138,1,25 +163,138,35106,2,0,L|196:138,1,25 +163,138,35224,2,0,L|196:138,1,25 + +// Reversed sliders +166,76,35695,2,0,L|327:75,1,150 +315,75,36106,2,0,L|158:76,1,150 +166,76,36518,2,0,L|327:75,1,150 +315,75,36929,2,0,L|158:76,1,150 diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json new file mode 100644 index 0000000000..fb4050963f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider-expected-conversion.json @@ -0,0 +1,222 @@ +{ + "Mappings": [{ + "StartTime": 369, + "Objects": [{ + "StartTime": 369, + "EndTime": 369, + "X": 177, + "Y": 191 + }, + { + "StartTime": 450, + "EndTime": 450, + "X": 216.539276, + "Y": 191.192871 + }, + { + "StartTime": 532, + "EndTime": 532, + "X": 256.5667, + "Y": 191.388138 + }, + { + "StartTime": 614, + "EndTime": 614, + "X": 296.594116, + "Y": 191.583389 + }, + { + "StartTime": 696, + "EndTime": 696, + "X": 336.621521, + "Y": 191.778641 + }, + { + "StartTime": 778, + "EndTime": 778, + "X": 376.648926, + "Y": 191.9739 + }, + { + "StartTime": 860, + "EndTime": 860, + "X": 337.318878, + "Y": 191.782043 + }, + { + "StartTime": 942, + "EndTime": 942, + "X": 297.291443, + "Y": 191.586792 + }, + { + "StartTime": 1024, + "EndTime": 1024, + "X": 257.264038, + "Y": 191.391541 + }, + { + "StartTime": 1106, + "EndTime": 1106, + "X": 217.2366, + "Y": 191.196274 + }, + { + "StartTime": 1188, + "EndTime": 1188, + "X": 177.209213, + "Y": 191.001022 + }, + { + "StartTime": 1270, + "EndTime": 1270, + "X": 216.818192, + "Y": 191.194229 + }, + { + "StartTime": 1352, + "EndTime": 1352, + "X": 256.8456, + "Y": 191.3895 + }, + { + "StartTime": 1434, + "EndTime": 1434, + "X": 296.873047, + "Y": 191.584747 + }, + { + "StartTime": 1516, + "EndTime": 1516, + "X": 336.900452, + "Y": 191.78 + }, + { + "StartTime": 1598, + "EndTime": 1598, + "X": 376.927917, + "Y": 191.975266 + }, + { + "StartTime": 1680, + "EndTime": 1680, + "X": 337.039948, + "Y": 191.780685 + }, + { + "StartTime": 1762, + "EndTime": 1762, + "X": 297.0125, + "Y": 191.585434 + }, + { + "StartTime": 1844, + "EndTime": 1844, + "X": 256.9851, + "Y": 191.390167 + }, + { + "StartTime": 1926, + "EndTime": 1926, + "X": 216.957672, + "Y": 191.194916 + }, + { + "StartTime": 2008, + "EndTime": 2008, + "X": 177.069717, + "Y": 191.000336 + }, + { + "StartTime": 2090, + "EndTime": 2090, + "X": 217.097137, + "Y": 191.1956 + }, + { + "StartTime": 2172, + "EndTime": 2172, + "X": 257.124573, + "Y": 191.390854 + }, + { + "StartTime": 2254, + "EndTime": 2254, + "X": 297.152, + "Y": 191.5861 + }, + { + "StartTime": 2336, + "EndTime": 2336, + "X": 337.179443, + "Y": 191.781372 + }, + { + "StartTime": 2418, + "EndTime": 2418, + "X": 376.7884, + "Y": 191.974579 + }, + { + "StartTime": 2500, + "EndTime": 2500, + "X": 336.760956, + "Y": 191.779327 + }, + { + "StartTime": 2582, + "EndTime": 2582, + "X": 296.733643, + "Y": 191.584076 + }, + { + "StartTime": 2664, + "EndTime": 2664, + "X": 256.7062, + "Y": 191.388809 + }, + { + "StartTime": 2746, + "EndTime": 2746, + "X": 216.678772, + "Y": 191.193558 + }, + { + "StartTime": 2828, + "EndTime": 2828, + "X": 177.348663, + "Y": 191.0017 + }, + { + "StartTime": 2909, + "EndTime": 2909, + "X": 216.887909, + "Y": 191.19458 + }, + { + "StartTime": 2991, + "EndTime": 2991, + "X": 256.915344, + "Y": 191.389832 + }, + { + "StartTime": 3073, + "EndTime": 3073, + "X": 296.942749, + "Y": 191.585083 + }, + { + "StartTime": 3155, + "EndTime": 3155, + "X": 336.970184, + "Y": 191.78035 + }, + { + "StartTime": 3201, + "EndTime": 3201, + "X": 376.99762, + "Y": 191.9756 + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider.osu new file mode 100644 index 0000000000..624d905384 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/repeat-slider.osu @@ -0,0 +1,18 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8 +SliderMultiplier:1.6 +SliderTickRate:4 + +[TimingPoints] +369,327.868852459016,4,2,2,32,1,0 + +[HitObjects] +177,191,369,6,0,L|382:192,7,200 \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json new file mode 100644 index 0000000000..12d1645c04 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider-expected-conversion.json @@ -0,0 +1,348 @@ +{ + "Mappings": [{ + "StartTime": 369, + "Objects": [{ + "StartTime": 369, + "EndTime": 369, + "X": 127, + "Y": 194 + }, + { + "StartTime": 450, + "EndTime": 450, + "X": 166.53389, + "Y": 193.8691 + }, + { + "StartTime": 532, + "EndTime": 532, + "X": 206.555847, + "Y": 193.736572 + }, + { + "StartTime": 614, + "EndTime": 614, + "X": 246.57782, + "Y": 193.60405 + }, + { + "StartTime": 696, + "EndTime": 696, + "X": 286.5998, + "Y": 193.471527 + }, + { + "StartTime": 778, + "EndTime": 778, + "X": 326.621765, + "Y": 193.339 + }, + { + "StartTime": 860, + "EndTime": 860, + "X": 366.6437, + "Y": 193.206482 + }, + { + "StartTime": 942, + "EndTime": 942, + "X": 406.66568, + "Y": 193.073959 + }, + { + "StartTime": 970, + "EndTime": 970, + "X": 420.331726, + "Y": 193.0287 + }, + { + "StartTime": 997, + "EndTime": 997, + "X": 407.153748, + "Y": 193.072342 + }, + { + "StartTime": 1079, + "EndTime": 1079, + "X": 367.131775, + "Y": 193.204865 + }, + { + "StartTime": 1161, + "EndTime": 1161, + "X": 327.1098, + "Y": 193.337387 + }, + { + "StartTime": 1243, + "EndTime": 1243, + "X": 287.08783, + "Y": 193.46991 + }, + { + "StartTime": 1325, + "EndTime": 1325, + "X": 247.0659, + "Y": 193.602432 + }, + { + "StartTime": 1407, + "EndTime": 1407, + "X": 207.043915, + "Y": 193.734955 + }, + { + "StartTime": 1489, + "EndTime": 1489, + "X": 167.021988, + "Y": 193.867477 + }, + { + "StartTime": 1571, + "EndTime": 1571, + "X": 127, + "Y": 194 + }, + { + "StartTime": 1653, + "EndTime": 1653, + "X": 167.021988, + "Y": 193.867477 + }, + { + "StartTime": 1735, + "EndTime": 1735, + "X": 207.043976, + "Y": 193.734955 + }, + { + "StartTime": 1817, + "EndTime": 1817, + "X": 247.065887, + "Y": 193.602432 + }, + { + "StartTime": 1899, + "EndTime": 1899, + "X": 287.08783, + "Y": 193.46991 + }, + { + "StartTime": 1981, + "EndTime": 1981, + "X": 327.1098, + "Y": 193.337387 + }, + { + "StartTime": 2062, + "EndTime": 2062, + "X": 366.643738, + "Y": 193.206482 + }, + { + "StartTime": 2144, + "EndTime": 2144, + "X": 406.665649, + "Y": 193.073959 + }, + { + "StartTime": 2172, + "EndTime": 2172, + "X": 420.331726, + "Y": 193.0287 + }, + { + "StartTime": 2199, + "EndTime": 2199, + "X": 407.153748, + "Y": 193.072342 + }, + { + "StartTime": 2281, + "EndTime": 2281, + "X": 367.1318, + "Y": 193.204865 + }, + { + "StartTime": 2363, + "EndTime": 2363, + "X": 327.1098, + "Y": 193.337387 + }, + { + "StartTime": 2445, + "EndTime": 2445, + "X": 287.08783, + "Y": 193.46991 + }, + { + "StartTime": 2527, + "EndTime": 2527, + "X": 247.065887, + "Y": 193.602432 + }, + { + "StartTime": 2609, + "EndTime": 2609, + "X": 207.043976, + "Y": 193.734955 + }, + { + "StartTime": 2691, + "EndTime": 2691, + "X": 167.021988, + "Y": 193.867477 + }, + { + "StartTime": 2773, + "EndTime": 2773, + "X": 127, + "Y": 194 + }, + { + "StartTime": 2855, + "EndTime": 2855, + "X": 167.021988, + "Y": 193.867477 + }, + { + "StartTime": 2937, + "EndTime": 2937, + "X": 207.043976, + "Y": 193.734955 + }, + { + "StartTime": 3019, + "EndTime": 3019, + "X": 247.065948, + "Y": 193.602432 + }, + { + "StartTime": 3101, + "EndTime": 3101, + "X": 287.087952, + "Y": 193.46991 + }, + { + "StartTime": 3183, + "EndTime": 3183, + "X": 327.109772, + "Y": 193.337387 + }, + { + "StartTime": 3265, + "EndTime": 3265, + "X": 367.131775, + "Y": 193.204865 + }, + { + "StartTime": 3347, + "EndTime": 3347, + "X": 407.153748, + "Y": 193.072342 + }, + { + "StartTime": 3374, + "EndTime": 3374, + "X": 420.331726, + "Y": 193.0287 + }, + { + "StartTime": 3401, + "EndTime": 3401, + "X": 407.153748, + "Y": 193.072342 + }, + { + "StartTime": 3483, + "EndTime": 3483, + "X": 367.131775, + "Y": 193.204865 + }, + { + "StartTime": 3565, + "EndTime": 3565, + "X": 327.109772, + "Y": 193.337387 + }, + { + "StartTime": 3647, + "EndTime": 3647, + "X": 287.087952, + "Y": 193.46991 + }, + { + "StartTime": 3729, + "EndTime": 3729, + "X": 247.065948, + "Y": 193.602432 + }, + { + "StartTime": 3811, + "EndTime": 3811, + "X": 207.043976, + "Y": 193.734955 + }, + { + "StartTime": 3893, + "EndTime": 3893, + "X": 167.021988, + "Y": 193.867477 + }, + { + "StartTime": 3975, + "EndTime": 3975, + "X": 127, + "Y": 194 + }, + { + "StartTime": 4057, + "EndTime": 4057, + "X": 167.021988, + "Y": 193.867477 + }, + { + "StartTime": 4139, + "EndTime": 4139, + "X": 207.043976, + "Y": 193.734955 + }, + { + "StartTime": 4221, + "EndTime": 4221, + "X": 247.065948, + "Y": 193.602432 + }, + { + "StartTime": 4303, + "EndTime": 4303, + "X": 287.087952, + "Y": 193.46991 + }, + { + "StartTime": 4385, + "EndTime": 4385, + "X": 327.109772, + "Y": 193.337387 + }, + { + "StartTime": 4467, + "EndTime": 4467, + "X": 367.131775, + "Y": 193.204865 + }, + { + "StartTime": 4540, + "EndTime": 4540, + "X": 420.331726, + "Y": 193.0287 + }, + { + "StartTime": 4549, + "EndTime": 4549, + "X": 407.153748, + "Y": 193.072342 + } + ] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider.osu new file mode 100644 index 0000000000..64aeeb0c07 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/uneven-repeat-slider.osu @@ -0,0 +1,19 @@ +osu file format v14 + +[General] +StackLeniency: 0.4 +Mode: 0 + +[Difficulty] +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8 +SliderMultiplier:1.6 +SliderTickRate:4 + +[TimingPoints] +369,327.868852459016,4,2,2,32,1,0 + +[HitObjects] +// A slider with an un-even amount of ticks +127,194,369,6,0,L|429:193,7,293.333333333333 diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/zero-length-sliders.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/zero-length-sliders.osu new file mode 100644 index 0000000000..18736043b5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/zero-length-sliders.osu @@ -0,0 +1,28 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:3 +CircleSize:3 +OverallDifficulty:3 +ApproachRate:4.5 +SliderMultiplier:0.799999999999999 +SliderTickRate:1 + +[TimingPoints] +800,260.869565217391,3,2,10,60,1,0 + +[HitObjects] +// Linear +78,193,2365,2,0,L|330:193,1,0 +78,193,3669,2,0,L|330:193,1,0 +78,193,4973,2,0,L|330:193,1,0 + +// Perfect-curve +151,206,6278,2,0,P|293:75|345:204,1,0 +151,206,8104,2,0,P|293:75|345:204,1,0 +151,206,9930,2,0,P|293:75|345:204,1,0 + +// Bezier +76,191,11756,2,0,B|176:59|358:340|438:190,1,0 +76,191,13582,2,0,B|176:59|358:340|438:190,1,0 +76,191,15408,2,0,B|176:59|358:340|438:190,1,0 diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index b986076593..05eb0ffdbf 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly TrailPart[] parts = new TrailPart[max_sprites]; private Vector2 size; - private readonly VertexBatch vertexBatch = new QuadBatch(max_sprites, 1); + private readonly TrailBatch vertexBatch = new TrailBatch(max_sprites, 1); public TrailDrawNode(CursorTrail source) : base(source) @@ -196,21 +196,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor for (int i = 0; i < parts.Length; ++i) { + vertexBatch.DrawTime = parts[i].Time; + Vector2 pos = parts[i].Position; - float localTime = parts[i].Time; DrawQuad( texture, new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y), DrawColourInfo.Colour, null, - v => vertexBatch.Add(new TexturedTrailVertex - { - Position = v.Position, - TexturePosition = v.TexturePosition, - Time = localTime + 1, - Colour = v.Colour, - })); + vertexBatch.AddAction); } shader.Unbind(); @@ -222,6 +217,25 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Dispose(); } + + // Todo: This shouldn't exist, but is currently used to reduce allocations by caching variable-capturing closures. + private class TrailBatch : QuadBatch + { + public new readonly Action AddAction; + public float DrawTime; + + public TrailBatch(int size, int maxBuffers) + : base(size, maxBuffers) + { + AddAction = v => Add(new TexturedTrailVertex + { + Position = v.Position, + TexturePosition = v.TexturePosition, + Time = DrawTime + 1, + Colour = v.Colour, + }); + } + } } [StructLayout(LayoutKind.Sequential)] diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 27546fa424..eb1977a13d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public class OsuCursor : SkinReloadableDrawable { + private const float size = 28; + private bool cursorExpand; private Bindable cursorScale; @@ -30,7 +32,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public OsuCursor() { Origin = Anchor.Centre; - Size = new Vector2(28); + + Size = new Vector2(size); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) @@ -46,66 +49,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - Child = scaleTarget = new SkinnableDrawable("cursor", _ => new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = Size.X / 6, - BorderColour = Color4.White, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Pink.Opacity(0.5f), - Radius = 5, - }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = Size.X / 3, - BorderColour = Color4.White.Opacity(0.5f), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - }, - }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.1f), - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - }, - }, - } - }, restrictSize: false) + Child = scaleTarget = new SkinnableDrawable("Play/osu/cursor", _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling) { Origin = Anchor.Centre, Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, } }; @@ -145,5 +92,76 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } public void Contract() => expandTarget.ScaleTo(released_scale, 100, Easing.OutQuad); + + private class DefaultCursor : CompositeDrawable + { + public DefaultCursor() + { + RelativeSizeAxes = Axes.Both; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = size / 6, + BorderColour = Color4.White, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Pink.Opacity(0.5f), + Radius = 5, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + new CircularContainer + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = size / 3, + BorderColour = Color4.White.Opacity(0.5f), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + }, + }, + new CircularContainer + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.1f), + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + }, + }, + } + } + }; + } + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 0cbe0cca85..9037faf606 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.UI; using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.UI { @@ -39,7 +40,13 @@ namespace osu.Game.Rulesets.Osu.UI RelativeSizeAxes = Axes.Both, Depth = 1, }, - HitObjectContainer, + // Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal + // Todo: Remove when hitobjects are properly pooled + new LocalSkinOverrideContainer(null) + { + RelativeSizeAxes = Axes.Both, + Child = HitObjectContainer, + }, approachCircles = new ApproachCircleProxyContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index e28ff5f460..9c8be868b0 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -13,12 +13,15 @@ namespace osu.Game.Rulesets.Osu.UI protected override Container Content => content; private readonly Container content; + private const float playfield_size_adjust = 0.8f; + public OsuPlayfieldAdjustmentContainer() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(0.75f); + // Calculated from osu!stable as 512 (default gamefield size) / 640 (default window size) + Size = new Vector2(playfield_size_adjust); InternalChild = new Container { @@ -40,7 +43,19 @@ namespace osu.Game.Rulesets.Osu.UI { base.Update(); + // The following calculation results in a constant of 1.6 when OsuPlayfieldAdjustmentContainer + // is consuming the full game_size. This matches the osu-stable "magic ratio". + // + // game_size = DrawSizePreservingFillContainer.TargetSize = new Vector2(1024, 768) + // + // Parent is a 4:3 aspect enforced, using height as the constricting dimension + // Parent.ChildSize.X = min(game_size.X, game_size.Y * (4 / 3)) * playfield_size_adjust + // Parent.ChildSize.X = 819.2 + // + // Scale = 819.2 / 512 + // Scale = 1.6 Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X); + // Size = 0.625 Size = Vector2.Divide(Vector2.One, Scale); } } diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj index d2817b743c..3e46bb89af 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/osu.Game.Rulesets.Taiko.Tests.iOS.csproj @@ -13,14 +13,6 @@ - - libbass.a - PreserveNewest - - - libbass_fx.a - PreserveNewest - Linker.xml diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 68ae7544c2..6c1882b4e2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -20,10 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [NonParallelizable] [TestCase("basic")] [TestCase("slider-generating-drumroll")] - public new void Test(string name) - { - base.Test(name); - } + public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs index 6f9856df83..3c84d900a6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs @@ -247,10 +247,6 @@ namespace osu.Game.Rulesets.Taiko.Tests : base(hitObject) { } - - protected override void UpdateState(ArmedState state) - { - } } } } 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 5510c3a9d9..a5db1625d9 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 @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index f8909fb98c..bf89f7e15b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -53,9 +53,5 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Alpha = 0.75f }); } - - protected override void UpdateState(ArmedState state) - { - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 9b4df74a61..f4407a7b54 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -88,13 +88,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Miss); } - protected override void UpdateState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { switch (state) { case ArmedState.Hit: case ArmedState.Miss: - this.FadeOut(100).Expire(); + this.Delay(HitObject.Duration).FadeOut(100).Expire(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 9259c693d9..cef9a53deb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Great); } - protected override void UpdateState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { switch (state) { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 4c8d5d5204..fa45067210 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -92,55 +92,42 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Size = BaseSize * Parent.RelativeChildSize; } - protected override void UpdateState(ArmedState state) + protected override void UpdateStateTransforms(ArmedState state) { - var circlePiece = MainPiece as CirclePiece; - circlePiece?.FlashBox.FinishTransforms(); - - var offset = !AllJudged ? 0 : Time.Current - HitObject.StartTime; - - using (BeginDelayedSequence(HitObject.StartTime - Time.Current + offset, true)) + switch (state) { - switch (State.Value) - { - case ArmedState.Idle: - validActionPressed = false; + case ArmedState.Idle: + validActionPressed = false; - UnproxyContent(); - this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); - break; + UnproxyContent(); + this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); + break; - case ArmedState.Miss: - this.FadeOut(100) - .Expire(); - break; + case ArmedState.Miss: + this.FadeOut(100) + .Expire(); + break; - case ArmedState.Hit: - // If we're far enough away from the left stage, we should bring outselves in front of it - ProxyContent(); + case ArmedState.Hit: + // If we're far enough away from the left stage, we should bring outselves in front of it + ProxyContent(); - var flash = circlePiece?.FlashBox; + var flash = (MainPiece as CirclePiece)?.FlashBox; + flash?.FadeTo(0.9f).FadeOut(300); - if (flash != null) - { - flash.FadeTo(0.9f); - flash.FadeOut(300); - } + const float gravity_time = 300; + const float gravity_travel_height = 200; - const float gravity_time = 300; - const float gravity_travel_height = 200; + this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad); - this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad); + this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out) + .Then() + .MoveToY(gravity_travel_height * 2, gravity_time * 2, Easing.In); - this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out) - .Then() - .MoveToY(gravity_travel_height * 2, gravity_time * 2, Easing.In); + this.FadeOut(800) + .Expire(); - this.FadeOut(800) - .Expire(); - - break; - } + break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 98a2e8a721..108e42eea5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -18,9 +18,5 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { MainObject = mainObject; } - - protected override void UpdateState(ArmedState state) - { - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 5ec9dc61e2..094ad1230f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -25,6 +25,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + /// + /// Offset away from the start time of the swell at which the ring starts appearing. + /// + private const double ring_appear_offset = 100; + private readonly List ticks = new List(); private readonly Container bodyContainer; @@ -51,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Centre, Alpha = 0, RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Masking = true, Children = new[] { @@ -70,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RelativeSizeAxes = Axes.Both, Masking = true, BorderThickness = target_ring_thick_border, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Children = new Drawable[] { new Box @@ -179,26 +184,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateState(ArmedState state) + protected override void UpdateInitialTransforms() { - const float preempt = 100; - const float out_transition_time = 300; + base.UpdateInitialTransforms(); + + using (BeginAbsoluteSequence(HitObject.StartTime - ring_appear_offset, true)) + targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + + protected override void UpdateStateTransforms(ArmedState state) + { + const double transition_duration = 300; switch (state) { case ArmedState.Idle: - UnproxyContent(); expandingRing.FadeTo(0); - using (BeginAbsoluteSequence(HitObject.StartTime - preempt, true)) - targetRing.ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint); break; case ArmedState.Miss: case ArmedState.Hit: - this.FadeOut(out_transition_time, Easing.Out); - bodyContainer.ScaleTo(1.4f, out_transition_time); + using (BeginAbsoluteSequence(Time.Current, true)) + { + this.FadeOut(transition_duration, Easing.Out); + bodyContainer.ScaleTo(1.4f, transition_duration); + + Expire(); + } - Expire(); break; } } @@ -212,9 +225,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Make the swell stop at the hit target X = Math.Max(0, X); - double t = Math.Min(HitObject.StartTime, Time.Current); - if (t == HitObject.StartTime) + if (Time.Current >= HitObject.StartTime - ring_appear_offset) ProxyContent(); + else + UnproxyContent(); } private bool? lastWasCentre; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 41a8fd9a75..8b27d78101 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -21,10 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override void UpdateState(ArmedState state) - { - } - public override bool OnPressed(TaikoAction action) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index bd45b52d7b..5424ccb4de 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -78,10 +78,29 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public abstract bool OnPressed(TaikoAction action); public virtual bool OnReleased(TaikoAction action) => false; + public override double LifetimeStart + { + get => base.LifetimeStart; + set + { + base.LifetimeStart = value; + proxiedContent.LifetimeStart = value; + } + } + + public override double LifetimeEnd + { + get => base.LifetimeEnd; + set + { + base.LifetimeEnd = value; + proxiedContent.LifetimeEnd = value; + } + } + private class ProxiedContentContainer : Container { - public override double LifetimeStart => Parent?.LifetimeStart ?? base.LifetimeStart; - public override double LifetimeEnd => Parent?.LifetimeEnd ?? base.LifetimeEnd; + public override bool RemoveWhenNotAlive => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index b7db819717..d9c0664ecd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Colour = Color4.White, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0, AlwaysPresent = true } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 422ba748e3..299679b2c1 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Taiko.Replays { @@ -113,7 +114,13 @@ namespace osu.Game.Rulesets.Taiko.Replays else throw new InvalidOperationException("Unknown hit object type."); - Frames.Add(new TaikoReplayFrame(endTime + KEY_UP_DELAY)); + var nextHitObject = GetNextObject(i); // Get the next object that requires pressing the same button + + bool canDelayKeyUp = nextHitObject == null || nextHitObject.StartTime > endTime + KEY_UP_DELAY; + + double calculatedDelay = canDelayKeyUp ? KEY_UP_DELAY : (nextHitObject.StartTime - endTime) * 0.9; + + Frames.Add(new TaikoReplayFrame(endTime + calculatedDelay)); if (i < Beatmap.HitObjects.Count - 1) { @@ -127,5 +134,24 @@ namespace osu.Game.Rulesets.Taiko.Replays return Replay; } + + protected override HitObject GetNextObject(int currentIndex) + { + Type desiredType = Beatmap.HitObjects[currentIndex].GetType(); + + for (int i = currentIndex + 1; i < Beatmap.HitObjects.Count; i++) + { + var currentObj = Beatmap.HitObjects[i]; + + if (currentObj.GetType() == desiredType || + // Un-press all keys before a DrumRoll or Swell + currentObj is DrumRoll || currentObj is Swell) + { + return Beatmap.HitObjects[i]; + } + } + + return null; + } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index a67004e9c7..83356b77c2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Replays.Types; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Difficulty; @@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko case ModType.Fun: return new Mod[] { - new MultiMod(new ModWindUp(), new ModWindDown()) + new MultiMod(new ModWindUp(), new ModWindDown()) }; default: diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index aa37ff7008..d6866c7d25 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.UI Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Alpha = 0, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, }, centre = new Sprite { @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Size = new Vector2(0.7f), Alpha = 0, - Blending = BlendingMode.Additive + Blending = BlendingParameters.Additive } }; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7427a3235d..a10f70a344 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -44,9 +44,8 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly JudgementContainer judgementContainer; internal readonly HitTarget HitTarget; - private readonly Container topLevelHitContainer; - - private readonly Container barlineContainer; + private readonly ProxyContainer topLevelHitContainer; + private readonly ProxyContainer barlineContainer; private readonly Container overlayBackgroundContainer; private readonly Container backgroundContainer; @@ -97,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.UI { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, }, HitTarget = new HitTarget { @@ -108,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } }, - barlineContainer = new Container + barlineContainer = new ProxyContainer { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = HIT_TARGET_OFFSET } @@ -127,14 +126,14 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Blending = BlendingMode.Additive + Blending = BlendingParameters.Additive }, judgementContainer = new JudgementContainer { Name = "Judgements", RelativeSizeAxes = Axes.Y, Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Blending = BlendingMode.Additive + Blending = BlendingParameters.Additive }, } }, @@ -183,7 +182,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } }, - topLevelHitContainer = new Container + topLevelHitContainer = new ProxyContainer { Name = "Top level hit objects", RelativeSizeAxes = Axes.Both, @@ -256,5 +255,15 @@ namespace osu.Game.Rulesets.Taiko.UI break; } } + + private class ProxyContainer : LifetimeManagementContainer + { + public new MarginPadding Padding + { + set => base.Padding = value; + } + + public void Add(Drawable proxy) => AddInternal(proxy); + } } } diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index ea5ab699f3..5c0713b895 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -13,14 +13,6 @@ - - libbass.a - PreserveNewest - - - libbass_fx.a - PreserveNewest - Linker.xml diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index d087251e7e..535320530d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -482,5 +482,17 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(hitObjects[0].Samples[0].Bank, hitObjects[0].Samples[1].Bank); } } + + [Test] + public void TestInvalidEventStillPasses() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var badResStream = TestResources.OpenResource("invalid-events.osu")) + using (var badStream = new StreamReader(badResStream)) + { + Assert.DoesNotThrow(() => decoder.Decode(badStream)); + } + } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 971518909d..953763c95d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Beatmaps.Formats int spriteCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSprite)); int animationCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardAnimation)); - int sampleCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSample)); + int sampleCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSampleInfo)); Assert.AreEqual(15, spriteCount); Assert.AreEqual(1, animationCount); diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs new file mode 100644 index 0000000000..9fba0f1668 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class SliderEventGenerationTest + { + private const double start_time = 0; + private const double span_duration = 1000; + + [Test] + public void TestSingleSpan() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); + + Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); + Assert.That(events[0].Time, Is.EqualTo(start_time)); + + Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[1].Time, Is.EqualTo(span_duration / 2)); + + Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail)); + Assert.That(events[3].Time, Is.EqualTo(span_duration)); + } + + [Test] + public void TestRepeat() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); + + Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); + Assert.That(events[0].Time, Is.EqualTo(start_time)); + + Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[1].Time, Is.EqualTo(span_duration / 2)); + + Assert.That(events[2].Type, Is.EqualTo(SliderEventType.Repeat)); + Assert.That(events[2].Time, Is.EqualTo(span_duration)); + + Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[3].Time, Is.EqualTo(span_duration + span_duration / 2)); + + Assert.That(events[5].Type, Is.EqualTo(SliderEventType.Tail)); + Assert.That(events[5].Time, Is.EqualTo(2 * span_duration)); + } + + [Test] + public void TestNonEvenTicks() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); + + Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); + Assert.That(events[0].Time, Is.EqualTo(start_time)); + + Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[1].Time, Is.EqualTo(300)); + + Assert.That(events[2].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[2].Time, Is.EqualTo(600)); + + Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[3].Time, Is.EqualTo(900)); + + Assert.That(events[4].Type, Is.EqualTo(SliderEventType.Repeat)); + Assert.That(events[4].Time, Is.EqualTo(span_duration)); + + Assert.That(events[5].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[5].Time, Is.EqualTo(1100)); + + Assert.That(events[6].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[6].Time, Is.EqualTo(1400)); + + Assert.That(events[7].Type, Is.EqualTo(SliderEventType.Tick)); + Assert.That(events[7].Time, Is.EqualTo(1700)); + + Assert.That(events[9].Type, Is.EqualTo(SliderEventType.Tail)); + Assert.That(events[9].Time, Is.EqualTo(2 * span_duration)); + } + + [Test] + public void TestLegacyLastTickOffset() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); + + Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); + Assert.That(events[2].Time, Is.EqualTo(900)); + } + + [Test] + public void TestMinimumTickDistance() + { + const double velocity = 5; + const double min_distance = velocity * 10; + + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); + + Assert.Multiple(() => + { + int tickIndex = -1; + + while (++tickIndex < events.Length) + { + if (events[tickIndex].Type != SliderEventType.Tick) + continue; + + Assert.That(events[tickIndex].Time, Is.LessThan(span_duration - min_distance).Or.GreaterThan(span_duration + min_distance)); + } + }); + } + } +} diff --git a/osu.Game.Tests/NonVisual/FramedReplayinputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs similarity index 94% rename from osu.Game.Tests/NonVisual/FramedReplayinputHandlerTest.cs rename to osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 73387fa5ab..18cbd4e7c5 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayinputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using NUnit.Framework; using osu.Game.Replays; @@ -9,7 +10,7 @@ using osu.Game.Rulesets.Replays; namespace osu.Game.Tests.NonVisual { [TestFixture] - public class FramedReplayinputHandlerTest + public class FramedReplayInputHandlerTest { private Replay replay; private TestInputHandler handler; @@ -160,10 +161,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestRewindInsideImportantSection() { - // fast forward to important section - while (handler.SetFrameFromTime(3000) != null) - { - } + fastForwardToPoint(3000); setTime(4000, 4000); confirmCurrentFrame(4); @@ -205,10 +203,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestRewindOutOfImportantSection() { - // fast forward to important section - while (handler.SetFrameFromTime(3500) != null) - { - } + fastForwardToPoint(3500); confirmCurrentFrame(3); confirmNextFrame(4); @@ -227,6 +222,15 @@ namespace osu.Game.Tests.NonVisual confirmNextFrame(2); } + private void fastForwardToPoint(double destination) + { + for (int i = 0; i < 1000; i++) + if (handler.SetFrameFromTime(destination) == null) + return; + + throw new TimeoutException("Seek was never fulfilled"); + } + private void setTime(double set, double? expect) { Assert.AreEqual(expect, handler.SetFrameFromTime(set)); @@ -274,6 +278,7 @@ namespace osu.Game.Tests.NonVisual public TestInputHandler(Replay replay) : base(replay) { + FrameAccuratePlayback = true; } protected override double AllowedImportantTimeSpan => 1000; diff --git a/osu.Game.Tests/Resources/invalid-events.osu b/osu.Game.Tests/Resources/invalid-events.osu new file mode 100644 index 0000000000..df86b26dba --- /dev/null +++ b/osu.Game.Tests/Resources/invalid-events.osu @@ -0,0 +1,14 @@ +osu file format v14 + +[Events] +bad,event,this,should,fail +//Background and Video events +0,0,"machinetop_background.jpg",0,0 +//Break Periods +2,122474,140135 +//Storyboard Layer 0 (Background) +this,is,also,bad +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs similarity index 84% rename from osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs rename to osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs index 8b941e4633..3061a3a542 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimContainer.cs @@ -10,13 +10,13 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -36,7 +36,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Background { [TestFixture] - public class TestSceneBackgroundScreenBeatmap : ManualInputManagerTestScene + public class TestSceneUserDimContainer : ManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { @@ -50,26 +50,14 @@ namespace osu.Game.Tests.Visual.Background private DummySongSelect songSelect; private TestPlayerLoader playerLoader; private TestPlayer player; - private DatabaseContextFactory factory; private BeatmapManager manager; private RulesetStore rulesets; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - factory = new DatabaseContextFactory(LocalStorage); - factory.ResetDatabase(); - - using (var usage = factory.Get()) - usage.Migrate(); - - factory.ResetDatabase(); - - using (var usage = factory.Get()) - usage.Migrate(); - - Dependencies.Cache(rulesets = new RulesetStore(factory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); manager.Import(TestResources.GetTestBeatmapForImport()).Wait(); @@ -131,20 +119,20 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(); createFakeStoryboard(); - AddStep("Storyboard Enabled", () => + AddStep("Enable Storyboard", () => { player.ReplacesBackground.Value = true; player.StoryboardEnabled.Value = true; }); waitForDim(); - AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible()); - AddStep("Storyboard Disabled", () => + AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible); + AddStep("Disable Storyboard", () => { player.ReplacesBackground.Value = false; player.StoryboardEnabled.Value = false; }); waitForDim(); - AddAssert("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && player.IsStoryboardInvisible()); + AddAssert("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible); } /// @@ -161,22 +149,44 @@ namespace osu.Game.Tests.Visual.Background } /// - /// Check if the is properly accepting user-defined visual changes at all. + /// Ensure is properly accepting user-defined visual changes for a background. /// [Test] - public void DisableUserDimTest() + public void DisableUserDimBackgroundTest() { performFullSetup(); waitForDim(); AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); - AddStep("EnableUserDim disabled", () => songSelect.DimEnabled.Value = false); + AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false); waitForDim(); AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled()); - AddStep("EnableUserDim enabled", () => songSelect.DimEnabled.Value = true); + AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true); waitForDim(); AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } + /// + /// Ensure is properly accepting user-defined visual changes for a storyboard. + /// + [Test] + public void DisableUserDimStoryboardTest() + { + performFullSetup(); + createFakeStoryboard(); + AddStep("Enable Storyboard", () => + { + player.ReplacesBackground.Value = true; + player.StoryboardEnabled.Value = true; + }); + AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); + AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); + waitForDim(); + AddAssert("Storyboard is invisible", () => !player.IsStoryboardVisible); + AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false); + waitForDim(); + AddAssert("Storyboard is visible", () => player.IsStoryboardVisible); + } + /// /// Check if the visual settings container retains dim and blur when pausing /// @@ -241,14 +251,15 @@ namespace osu.Game.Tests.Visual.Background { player.StoryboardEnabled.Value = false; player.ReplacesBackground.Value = false; - player.CurrentStoryboardContainer.Add(new OsuSpriteText + player.DimmableStoryboard.Add(new OsuSpriteText { - Size = new Vector2(250, 50), + Size = new Vector2(500, 50), Alpha = 1, - Colour = Color4.Tomato, + Colour = Color4.White, Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "THIS IS A STORYBOARD", + Font = new FontUsage(size: 50) }); }); @@ -300,7 +311,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White; - public bool IsUserBlurApplied() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2((float)BlurLevel.Value * 25); + public bool IsUserBlurApplied() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR); public bool IsUserBlurDisabled() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(0); @@ -333,17 +344,7 @@ namespace osu.Game.Tests.Visual.Background { protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); - protected override UserDimContainer CreateStoryboardContainer() - { - return new TestUserDimContainer(true) - { - RelativeSizeAxes = Axes.Both, - Alpha = 1, - EnableUserDim = { Value = true } - }; - } - - public UserDimContainer CurrentStoryboardContainer => StoryboardContainer; + public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; // Whether or not the player should be allowed to load. public bool BlockLoad; @@ -357,9 +358,7 @@ namespace osu.Game.Tests.Visual.Background { } - public bool IsStoryboardVisible() => ((TestUserDimContainer)CurrentStoryboardContainer).CurrentAlpha == 1; - - public bool IsStoryboardInvisible() => ((TestUserDimContainer)CurrentStoryboardContainer).CurrentAlpha <= 1; + public bool IsStoryboardVisible => DimmableStoryboard.ContentDisplayed; [BackgroundDependencyLoader] private void load(OsuConfigManager config, CancellationToken token) @@ -392,15 +391,15 @@ namespace osu.Game.Tests.Visual.Background private class FadeAccessibleBackground : BackgroundScreenBeatmap { - protected override UserDimContainer CreateFadeContainer() => fadeContainer = new TestUserDimContainer { RelativeSizeAxes = Axes.Both }; + protected override DimmableBackground CreateFadeContainer() => dimmable = new TestDimmableBackground { RelativeSizeAxes = Axes.Both }; - public Color4 CurrentColour => fadeContainer.CurrentColour; + public Color4 CurrentColour => dimmable.CurrentColour; - public float CurrentAlpha => fadeContainer.CurrentAlpha; + public float CurrentAlpha => dimmable.CurrentAlpha; public Vector2 CurrentBlur => Background.BlurSigma; - private TestUserDimContainer fadeContainer; + private TestDimmableBackground dimmable; public FadeAccessibleBackground(WorkingBeatmap beatmap) : base(beatmap) @@ -408,15 +407,10 @@ namespace osu.Game.Tests.Visual.Background } } - private class TestUserDimContainer : UserDimContainer + private class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground { - public Color4 CurrentColour => DimContainer.Colour; - public float CurrentAlpha => DimContainer.Alpha; - - public TestUserDimContainer(bool isStoryboard = false) - : base(isStoryboard) - { - } + public Color4 CurrentColour => Content.Colour; + public float CurrentAlpha => Content.Alpha; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs index 3cd1b8307a..879e15c548 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; using osu.Game.Screens.Play; @@ -11,78 +14,172 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneBreakOverlay : OsuTestScene { - private readonly BreakOverlay breakOverlay; + public override IReadOnlyList RequiredTypes => new[] + { + typeof(BreakOverlay), + }; + + private readonly TestBreakOverlay breakOverlay; + + private readonly IReadOnlyList testBreaks = new List + { + new BreakPeriod + { + StartTime = 1000, + EndTime = 5000, + }, + new BreakPeriod + { + StartTime = 6000, + EndTime = 13500, + }, + }; public TestSceneBreakOverlay() { - Child = breakOverlay = new BreakOverlay(true); - - AddStep("2s break", () => startBreak(2000)); - AddStep("5s break", () => startBreak(5000)); - AddStep("10s break", () => startBreak(10000)); - AddStep("15s break", () => startBreak(15000)); - AddStep("2s, 2s", startMultipleBreaks); - AddStep("0.5s, 0.7s, 1s, 2s", startAnotherMultipleBreaks); + Add(breakOverlay = new TestBreakOverlay(true)); } - private void startBreak(double duration) + [Test] + public void TestShowBreaks() { - breakOverlay.Breaks = new List + setClock(false); + + addShowBreakStep(2); + addShowBreakStep(5); + addShowBreakStep(15); + } + + [Test] + public void TestNoEffectsBreak() + { + var shortBreak = new BreakPeriod { EndTime = 500 }; + + setClock(true); + loadBreaksStep("short break", new[] { shortBreak }); + + addBreakSeeks(shortBreak, false); + } + + [Test] + public void TestMultipleBreaks() + { + setClock(true); + loadBreaksStep("multiple breaks", testBreaks); + + foreach (var b in testBreaks) + addBreakSeeks(b, false); + } + + [Test] + public void TestRewindBreaks() + { + setClock(true); + loadBreaksStep("multiple breaks", testBreaks); + + foreach (var b in testBreaks.Reverse()) + addBreakSeeks(b, true); + } + + [Test] + public void TestSkipBreaks() + { + setClock(true); + loadBreaksStep("multiple breaks", testBreaks); + + seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true); + AddAssert("is skipped to break #2", () => breakOverlay.CurrentBreakIndex == 1); + + seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true); + seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false); + seekAndAssertBreak("seek to break after end", testBreaks[1].EndTime + 500, false); + } + + private void addShowBreakStep(double seconds) + { + AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List { new BreakPeriod { StartTime = Clock.CurrentTime, - EndTime = Clock.CurrentTime + duration, + EndTime = Clock.CurrentTime + seconds * 1000, } - }; + }); } - private void startMultipleBreaks() + private void setClock(bool useManual) { - double currentTime = Clock.CurrentTime; - - breakOverlay.Breaks = new List - { - new BreakPeriod - { - StartTime = currentTime, - EndTime = currentTime + 2000, - }, - new BreakPeriod - { - StartTime = currentTime + 4000, - EndTime = currentTime + 6000, - } - }; + AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakOverlay.SwitchClock(useManual)); } - private void startAnotherMultipleBreaks() + private void loadBreaksStep(string breakDescription, IReadOnlyList breaks) { - double currentTime = Clock.CurrentTime; + AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breaks); + seekAndAssertBreak("seek back to 0", 0, false); + } - breakOverlay.Breaks = new List + private void addBreakSeeks(BreakPeriod b, bool isReversed) + { + if (isReversed) { - new BreakPeriod // Duration is less than 650 - too short to appear - { - StartTime = currentTime, - EndTime = currentTime + 500, - }, - new BreakPeriod - { - StartTime = currentTime + 1500, - EndTime = currentTime + 2200, - }, - new BreakPeriod - { - StartTime = currentTime + 3200, - EndTime = currentTime + 4200, - }, - new BreakPeriod - { - StartTime = currentTime + 5200, - EndTime = currentTime + 7200, - } - }; + seekAndAssertBreak("seek to break after end", b.EndTime + 500, false); + seekAndAssertBreak("seek to break end", b.EndTime, false); + seekAndAssertBreak("seek to break middle", b.StartTime + b.Duration / 2, b.HasEffect); + seekAndAssertBreak("seek to break start", b.StartTime, b.HasEffect); + } + else + { + seekAndAssertBreak("seek to break start", b.StartTime, b.HasEffect); + seekAndAssertBreak("seek to break middle", b.StartTime + b.Duration / 2, b.HasEffect); + seekAndAssertBreak("seek to break end", b.EndTime, false); + seekAndAssertBreak("seek to break after end", b.EndTime + 500, false); + } + } + + private void seekAndAssertBreak(string seekStepDescription, double time, bool shouldBeBreak) + { + AddStep(seekStepDescription, () => breakOverlay.ManualClockTime = time); + AddAssert($"is{(!shouldBeBreak ? " not" : string.Empty)} break time", () => + { + breakOverlay.ProgressTime(); + return breakOverlay.IsBreakTime.Value == shouldBeBreak; + }); + } + + private class TestBreakOverlay : BreakOverlay + { + private readonly FramedClock framedManualClock; + private readonly ManualClock manualClock; + private IFrameBasedClock originalClock; + + public new int CurrentBreakIndex => base.CurrentBreakIndex; + + public double ManualClockTime + { + get => manualClock.CurrentTime; + set => manualClock.CurrentTime = value; + } + + public TestBreakOverlay(bool letterboxing) + : base(letterboxing) + { + framedManualClock = new FramedClock(manualClock = new ManualClock()); + ProcessCustomClock = false; + } + + public void ProgressTime() + { + framedManualClock.ProcessFrame(); + Update(); + } + + public void SwitchClock(bool setManual) => Clock = setManual ? framedManualClock : originalClock; + + protected override void LoadComplete() + { + base.LoadComplete(); + originalClock = Clock; + } } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs new file mode 100644 index 0000000000..ee11fc0d06 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -0,0 +1,305 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneDrawableScrollingRuleset : OsuTestScene + { + /// + /// The amount of time visible by the "view window" of the playfield. + /// All hitobjects added through are spaced apart by this value, such that for a beat length of 1000, + /// there will be at most 2 hitobjects visible in the "view window". + /// + private const double time_range = 1000; + + private readonly ManualClock testClock = new ManualClock(); + private TestDrawableScrollingRuleset drawableRuleset; + + [SetUp] + public void Setup() => Schedule(() => testClock.CurrentTime = 0); + + [Test] + public void TestRelativeBeatLengthScaleSingleTimingPoint() + { + var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range / 2 }); + + createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); + + assertPosition(0, 0f); + + // The single timing point is 1x speed relative to itself, such that the hitobject occurring time_range milliseconds later should appear + // at the bottom of the view window regardless of the timing point's beat length + assertPosition(1, 1f); + } + + [Test] + public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant() + { + var beatmap = createBeatmap( + new TimingControlPoint { BeatLength = time_range / 2 }, + new TimingControlPoint { Time = 12000, BeatLength = time_range }, + new TimingControlPoint { Time = 100000, BeatLength = time_range }); + + createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); + + assertPosition(0, 0f); + assertPosition(1, 1f); + } + + [Test] + public void TestRelativeBeatLengthScaleFromSecondTimingPoint() + { + var beatmap = createBeatmap( + new TimingControlPoint { BeatLength = time_range }, + new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 }); + + createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); + + // The first timing point should have a relative velocity of 2 + assertPosition(0, 0f); + assertPosition(1, 0.5f); + assertPosition(2, 1f); + + // Move to the second timing point + setTime(3 * time_range); + assertPosition(3, 0f); + + // As above, this is the timing point that is 1x speed relative to itself, so the hitobject occurring time_range milliseconds later should be at the bottom of the view window + assertPosition(4, 1f); + } + + [Test] + public void TestNonRelativeScale() + { + var beatmap = createBeatmap( + new TimingControlPoint { BeatLength = time_range }, + new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 }); + + createTest(beatmap); + + assertPosition(0, 0f); + assertPosition(1, 1); + + // Move to the second timing point + setTime(3 * time_range); + assertPosition(3, 0f); + + // For a beat length of 500, the view window of this timing point is elongated 2x (1000 / 500), such that the second hitobject is two TimeRanges away (offscreen) + // To bring it on-screen, half TimeRange is added to the current time, bringing the second half of the view window into view, and the hitobject should appear at the bottom + setTime(3 * time_range + time_range / 2); + assertPosition(4, 1f); + } + + private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}", + () => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY)); + + private void setTime(double time) + { + AddStep($"set time = {time}", () => testClock.CurrentTime = time); + } + + /// + /// Creates an , containing 10 hitobjects and user-provided timing points. + /// The hitobjects are spaced milliseconds apart. + /// + /// The timing points to add to the beatmap. + /// The . + private IBeatmap createBeatmap(params TimingControlPoint[] timingControlPoints) + { + var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; + + beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints); + + for (int i = 0; i < 10; i++) + beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range }); + + return beatmap; + } + + private void createTest(IBeatmap beatmap, Action overrideAction = null) => AddStep("create test", () => + { + var ruleset = new TestScrollingRuleset(); + + drawableRuleset = (TestDrawableScrollingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap), Array.Empty()); + drawableRuleset.FrameStablePlayback = false; + + overrideAction?.Invoke(drawableRuleset); + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 0.75f, + Width = 400, + Masking = true, + Clock = new FramedClock(testClock), + Child = drawableRuleset + }; + }); + + #region Ruleset + + private class TestScrollingRuleset : Ruleset + { + public TestScrollingRuleset(RulesetInfo rulesetInfo = null) + : base(rulesetInfo) + { + } + + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new TestDrawableScrollingRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); + + public override string Description { get; } = string.Empty; + + public override string ShortName { get; } = string.Empty; + } + + private class TestDrawableScrollingRuleset : DrawableScrollingRuleset + { + public bool RelativeScaleBeatLengthsOverride { get; set; } + + protected override bool RelativeScaleBeatLengths => RelativeScaleBeatLengthsOverride; + + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; + + public TestDrawableScrollingRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods) + : base(ruleset, beatmap, mods) + { + TimeRange.Value = time_range; + } + + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h); + + protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); + + protected override Playfield CreatePlayfield() => new TestPlayfield(); + } + + private class TestPlayfield : ScrollingPlayfield + { + public TestPlayfield() + { + AddInternal(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 150 }, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 2, + Colour = Color4.Green + }, + HitObjectContainer + } + } + } + }); + } + } + + private class TestBeatmapConverter : BeatmapConverter + { + public TestBeatmapConverter(IBeatmap beatmap) + : base(beatmap) + { + } + + protected override IEnumerable ValidConversionTypes => new[] { typeof(HitObject) }; + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) + { + yield return new TestHitObject + { + StartTime = original.StartTime, + EndTime = (original as IHasEndTime)?.EndTime ?? (original.StartTime + 100) + }; + } + } + + #endregion + + #region HitObject + + private class TestHitObject : HitObject, IHasEndTime + { + public double EndTime { get; set; } + + public double Duration => EndTime - StartTime; + } + + private class DrawableTestHitObject : DrawableHitObject + { + public DrawableTestHitObject(TestHitObject hitObject) + : base(hitObject) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + Size = new Vector2(100, 25); + + AddRangeInternal(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.LightPink + }, + new Box + { + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = 2, + Colour = Color4.Red + } + }); + } + } + + #endregion + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index d42b61ea55..0c5ead10cf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -17,6 +17,8 @@ namespace osu.Game.Tests.Visual.Gameplay { private bool exitAction; + protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 0a9cdc6a8e..aa80819694 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -200,10 +200,6 @@ namespace osu.Game.Tests.Visual.Gameplay break; } } - - protected override void UpdateState(ArmedState state) - { - } } private class TestDrawableHitObject : DrawableHitObject @@ -216,10 +212,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddInternal(new Box { Size = new Vector2(75) }); } - - protected override void UpdateState(ArmedState state) - { - } } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs deleted file mode 100644 index c7a0df6e9f..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinReloadable.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Textures; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Skinning; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public class TestSceneSkinReloadable : OsuTestScene - { - [Test] - public void TestInitialLoad() - { - var secondarySource = new SecondarySource(); - SkinConsumer consumer = null; - - AddStep("setup layout", () => - { - Child = new SkinSourceContainer - { - RelativeSizeAxes = Axes.Both, - Child = new LocalSkinOverrideContainer(secondarySource) - { - RelativeSizeAxes = Axes.Both, - Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true) - } - }; - }); - - AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); - AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); - } - - [Test] - public void TestOverride() - { - var secondarySource = new SecondarySource(); - - SkinConsumer consumer = null; - Container target = null; - - AddStep("setup layout", () => - { - Child = new SkinSourceContainer - { - RelativeSizeAxes = Axes.Both, - Child = target = new LocalSkinOverrideContainer(secondarySource) - { - RelativeSizeAxes = Axes.Both, - } - }; - }); - - AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); - AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); - AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); - } - - private class NamedBox : Container - { - public NamedBox(string name) - { - Children = new Drawable[] - { - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Font = OsuFont.Default.With(size: 40), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = name - } - }; - } - } - - private class SkinConsumer : SkinnableDrawable - { - public new Drawable Drawable => base.Drawable; - public int SkinChangedCount { get; private set; } - - public SkinConsumer(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : base(name, defaultImplementation, allowFallback, restrictSize) - { - } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - SkinChangedCount++; - } - } - - private class BaseSourceBox : NamedBox - { - public BaseSourceBox() - : base("Base Source") - { - } - } - - private class SecondarySourceBox : NamedBox - { - public SecondarySourceBox() - : base("Secondary Source") - { - } - } - - private class SecondarySource : ISkin - { - public Drawable GetDrawableComponent(string componentName) => new SecondarySourceBox(); - - public Texture GetTexture(string componentName) => throw new NotImplementedException(); - - public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); - - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); - } - - private class SkinSourceContainer : Container, ISkin - { - public Drawable GetDrawableComponent(string componentName) => new BaseSourceBox(); - - public Texture GetTexture(string componentName) => throw new NotImplementedException(); - - public SampleChannel GetSample(string sampleName) => throw new NotImplementedException(); - - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs new file mode 100644 index 0000000000..6c003e62ec --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -0,0 +1,284 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableDrawable : OsuTestScene + { + [Test] + public void TestConfineScaleDown() + { + FillFlowContainer fill = null; + + AddStep("setup layout larger source", () => + { + Child = new LocalSkinOverrideContainer(new SizedSource(50)) + { + RelativeSizeAxes = Axes.Both, + Child = fill = new FillFlowContainer + { + Size = new Vector2(30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10), + Children = new[] + { + new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) + } + }, + }; + }); + + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + AddStep("adjust scale", () => fill.Scale = new Vector2(2)); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + } + + [Test] + public void TestConfineScaleUp() + { + FillFlowContainer fill = null; + + AddStep("setup layout larger source", () => + { + Child = new LocalSkinOverrideContainer(new SizedSource(30)) + { + RelativeSizeAxes = Axes.Both, + Child = fill = new FillFlowContainer + { + Size = new Vector2(50), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10), + Children = new[] + { + new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), + new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) + } + }, + }; + }); + + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + AddStep("adjust scale", () => fill.Scale = new Vector2(2)); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + } + + [Test] + public void TestInitialLoad() + { + var secondarySource = new SecondarySource(); + SkinConsumer consumer = null; + + AddStep("setup layout", () => + { + Child = new SkinSourceContainer + { + RelativeSizeAxes = Axes.Both, + Child = new LocalSkinOverrideContainer(secondarySource) + { + RelativeSizeAxes = Axes.Both, + Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true) + } + }; + }); + + AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); + AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); + } + + [Test] + public void TestOverride() + { + var secondarySource = new SecondarySource(); + + SkinConsumer consumer = null; + Container target = null; + + AddStep("setup layout", () => + { + Child = new SkinSourceContainer + { + RelativeSizeAxes = Axes.Both, + Child = target = new LocalSkinOverrideContainer(secondarySource) + { + RelativeSizeAxes = Axes.Both, + } + }; + }); + + AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); + AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); + AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); + } + + private class ExposedSkinnableDrawable : SkinnableDrawable + { + public new Drawable Drawable => base.Drawable; + + public ExposedSkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, defaultImplementation, allowFallback, confineMode) + { + } + } + + private class DefaultBox : DrawWidthBox + { + public DefaultBox() + { + RelativeSizeAxes = Axes.Both; + } + } + + private class DrawWidthBox : Container + { + private readonly OsuSpriteText text; + + public DrawWidthBox() + { + Children = new Drawable[] + { + new Box + { + Colour = Color4.Gray, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + text.Text = DrawWidth.ToString(CultureInfo.InvariantCulture); + } + } + + private class NamedBox : Container + { + public NamedBox(string name) + { + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Font = OsuFont.Default.With(size: 40), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = name + } + }; + } + } + + private class SkinConsumer : SkinnableDrawable + { + public new Drawable Drawable => base.Drawable; + public int SkinChangedCount { get; private set; } + + public SkinConsumer(string name, Func defaultImplementation, Func allowFallback = null) + : base(name, defaultImplementation, allowFallback) + { + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + SkinChangedCount++; + } + } + + private class BaseSourceBox : NamedBox + { + public BaseSourceBox() + : base("Base Source") + { + } + } + + private class SecondarySourceBox : NamedBox + { + public SecondarySourceBox() + : base("Secondary Source") + { + } + } + + private class SizedSource : ISkin + { + private readonly float size; + + public SizedSource(float size) + { + this.size = size; + } + + public Drawable GetDrawableComponent(string componentName) => + componentName == "available" + ? new DrawWidthBox + { + Colour = Color4.Yellow, + Size = new Vector2(size) + } + : null; + + public Texture GetTexture(string componentName) => throw new NotImplementedException(); + + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + } + + private class SecondarySource : ISkin + { + public Drawable GetDrawableComponent(string componentName) => new SecondarySourceBox(); + + public Texture GetTexture(string componentName) => throw new NotImplementedException(); + + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + } + + private class SkinSourceContainer : Container, ISkin + { + public Drawable GetDrawableComponent(string componentName) => new BaseSourceBox(); + + public Texture GetTexture(string componentName) => throw new NotImplementedException(); + + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 0519660477..b152c21454 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -1,19 +1,88 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Screens.Play; +using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneSkipOverlay : OsuTestScene + public class TestSceneSkipOverlay : ManualInputManagerTestScene { - protected override void LoadComplete() - { - base.LoadComplete(); + private SkipOverlay skip; + private int requestCount; - Add(new SkipOverlay(Clock.CurrentTime + 5000)); + [SetUp] + public void SetUp() => Schedule(() => + { + requestCount = 0; + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedOffsetClock(Clock) + { + Offset = -Clock.CurrentTime, + }, + Children = new Drawable[] + { + skip = new SkipOverlay(6000) + { + RequestSeek = _ => requestCount++ + } + }, + }; + }); + + [Test] + public void TestFadeOnIdle() + { + AddStep("move mouse", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddUntilStep("fully visible", () => skip.Children.First().Alpha == 1); + AddUntilStep("wait for fade", () => skip.Children.First().Alpha < 1); + + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("fully visible", () => skip.Children.First().Alpha == 1); + AddUntilStep("wait for fade", () => skip.Children.First().Alpha < 1); } + + [Test] + public void TestClickableAfterFade() + { + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for fade", () => skip.Children.First().Alpha == 0); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkRequestCount(1); + } + + [Test] + public void TestClickOnlyActuatesOnce() + { + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkRequestCount(1); + } + + [Test] + public void TestDoesntFadeOnMouseDown() + { + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); + AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); + AddUntilStep("wait for overlay disapper", () => !skip.IsAlive); + AddAssert("ensure button didn't disappear", () => skip.Children.First().Alpha > 0); + AddStep("button up", () => InputManager.ReleaseButton(MouseButton.Left)); + checkRequestCount(0); + } + + private void checkRequestCount(int expected) => + AddAssert($"request count is {expected}", () => requestCount == expected); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index ead7a4b7fc..ff8437311e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -21,32 +21,38 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Container storyboardContainer; private DrawableStoryboard storyboard; + [Cached] + private MusicController musicController = new MusicController(); + public TestSceneStoryboard() { Clock = new FramedClock(); - Add(new Container + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + musicController, + new Container { - new Box + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - storyboardContainer = new Container - { - RelativeSizeAxes = Axes.Both, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + storyboardContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, }, }, - }); - - Add(new MusicController - { - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - State = { Value = Visibility.Visible }, + new NowPlayingOverlay + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + State = { Value = Visibility.Visible }, + } }); AddStep("Restart", restart); diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs new file mode 100644 index 0000000000..d03d341ee4 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Screens; +using osu.Game.Screens.Menu; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public abstract class IntroTestScene : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(StartupScreen), + typeof(IntroScreen), + typeof(OsuScreen), + typeof(IntroTestScene), + }; + + [Cached] + private OsuLogo logo; + + protected IntroTestScene() + { + Drawable introStack = null; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = Color4.Black, + }, + logo = new OsuLogo + { + Alpha = 0, + RelativePositionAxes = Axes.Both, + Depth = float.MinValue, + Position = new Vector2(0.5f), + } + }; + + AddStep("restart sequence", () => + { + logo.FinishTransforms(); + logo.IsTracking = false; + + introStack?.Expire(); + + Add(introStack = new OsuScreenStack(CreateScreen()) + { + RelativeSizeAxes = Axes.Both, + }); + }); + } + + protected abstract IScreen CreateScreen(); + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs new file mode 100644 index 0000000000..107734cc8d --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public class TestSceneIntroCircles : IntroTestScene + { + protected override IScreen CreateScreen() => new IntroCircles(); + } +} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroSequence.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroSequence.cs deleted file mode 100644 index b59fb18428..0000000000 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroSequence.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; -using osu.Game.Screens.Menu; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Menus -{ - [TestFixture] - public class TestSceneIntroSequence : OsuTestScene - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuLogo), - }; - - public TestSceneIntroSequence() - { - OsuLogo logo; - - var rateAdjustClock = new StopwatchClock(true); - var framedClock = new FramedClock(rateAdjustClock); - framedClock.ProcessFrame(); - - Add(new Container - { - RelativeSizeAxes = Axes.Both, - Clock = framedClock, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - logo = new OsuLogo - { - Anchor = Anchor.Centre, - } - } - }); - - AddStep(@"Restart", logo.PlayIntro); - AddSliderStep("Playback speed", 0.0, 2.0, 1, v => rateAdjustClock.Rate = v); - } - } -} diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs new file mode 100644 index 0000000000..df79584167 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public class TestSceneIntroTriangles : IntroTestScene + { + protected override IScreen CreateScreen() => new IntroTriangles(); + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs new file mode 100644 index 0000000000..68ad0b42b4 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapPanel.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Match.Components; +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Game.Audio; +using osu.Framework.Allocation; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + [Cached(typeof(IPreviewTrackOwner))] + public class TestSceneMatchBeatmapPanel : MultiplayerTestScene, IPreviewTrackOwner + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(MatchBeatmapPanel) + }; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } + + public TestSceneMatchBeatmapPanel() + { + Add(new MatchBeatmapPanel + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1763072 } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2101557 } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1973466 } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2109801 } }); + Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1922035 } }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("Select random beatmap", () => + { + Room.CurrentItem.Value = Room.Playlist[RNG.Next(Room.Playlist.Count)]; + previewTrackManager.StopAnyPlaying(this); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs index 3f0c0b07b7..a6c036a876 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchInfo.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer typeof(Info), typeof(HeaderButton), typeof(ReadyButton), - typeof(ViewBeatmapButton) + typeof(MatchBeatmapPanel) }; [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index d87d9910c3..2d918442eb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -135,6 +135,9 @@ namespace osu.Game.Tests.Visual.Online }); downloadAssert(true); + + AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet())); + downloadAssert(true); } [Test] @@ -224,6 +227,56 @@ namespace osu.Game.Tests.Visual.Online AddStep(@"show without reload", overlay.Show); } + private BeatmapSetInfo createManyDifficultiesBeatmapSet() + { + var beatmaps = new List(); + + for (int i = 1; i < 41; i++) + { + beatmaps.Add(new BeatmapInfo + { + OnlineBeatmapID = i * 10, + Version = $"Test #{i}", + Ruleset = Ruleset.Value, + StarDifficulty = 2 + i * 0.1, + BaseDifficulty = new BeatmapDifficulty + { + OverallDifficulty = 3.5f, + }, + OnlineInfo = new BeatmapOnlineInfo(), + Metrics = new BeatmapMetrics + { + Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(), + Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(), + }, + }); + } + + return new BeatmapSetInfo + { + OnlineBeatmapSetID = 123, + Metadata = new BeatmapMetadata + { + Title = @"many difficulties beatmap", + Artist = @"none", + Author = new User + { + Username = @"BanchoBot", + Id = 3, + }, + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Preview = @"https://b.ppy.sh/preview/123.mp3", + HasVideo = true, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), + }, + Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }, + Beatmaps = beatmaps, + }; + } + private void downloadAssert(bool shown) { AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.DownloadButtonsVisible == shown); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 4d3992ce13..9196513a55 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Online private TestChatOverlay chatOverlay; private ChannelManager channelManager; - private readonly Channel channel1 = new Channel(new User()) { Name = "test1" }; + private readonly Channel channel1 = new Channel(new User()) { Name = "test really long username" }; private readonly Channel channel2 = new Channel(new User()) { Name = "test2" }; [SetUp] diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index 53dbaeddda..731cb62518 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Overlays.Direct; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; using osu.Game.Users; using osuTK; @@ -24,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online typeof(IconPill) }; - private BeatmapSetInfo getUndownloadableBeatmapSet(RulesetInfo ruleset) => new BeatmapSetInfo + private BeatmapSetInfo getUndownloadableBeatmapSet() => new BeatmapSetInfo { OnlineBeatmapSetID = 123, Metadata = new BeatmapMetadata @@ -56,23 +55,62 @@ namespace osu.Game.Tests.Visual.Online { new BeatmapInfo { - Ruleset = ruleset, + Ruleset = Ruleset.Value, Version = "Test", StarDifficulty = 6.42, } } }; - [BackgroundDependencyLoader] - private void load() + private BeatmapSetInfo getManyDifficultiesBeatmapSet(RulesetStore rulesets) { - var ruleset = new OsuRuleset().RulesetInfo; + var beatmaps = new List(); - var normal = CreateWorkingBeatmap(ruleset).BeatmapSetInfo; + for (int i = 0; i < 100; i++) + { + beatmaps.Add(new BeatmapInfo + { + Ruleset = rulesets.GetRuleset(i % 4), + StarDifficulty = 2 + i % 4 * 2, + BaseDifficulty = new BeatmapDifficulty + { + OverallDifficulty = 3.5f, + } + }); + } + + return new BeatmapSetInfo + { + OnlineBeatmapSetID = 1, + Metadata = new BeatmapMetadata + { + Title = "many difficulties beatmap", + Artist = "test", + Author = new User + { + Username = "BanchoBot", + Id = 3, + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + HasVideo = true, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), + }, + Beatmaps = beatmaps, + }; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + var normal = CreateWorkingBeatmap(Ruleset.Value).BeatmapSetInfo; normal.OnlineInfo.HasVideo = true; normal.OnlineInfo.HasStoryboard = true; - var undownloadable = getUndownloadableBeatmapSet(ruleset); + var undownloadable = getUndownloadableBeatmapSet(); + var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets); Child = new BasicScrollContainer { @@ -81,15 +119,17 @@ namespace osu.Game.Tests.Visual.Online { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, + Direction = FillDirection.Full, Padding = new MarginPadding(20), - Spacing = new Vector2(0, 20), + Spacing = new Vector2(5, 20), Children = new Drawable[] { new DirectGridPanel(normal), - new DirectListPanel(normal), new DirectGridPanel(undownloadable), + new DirectGridPanel(manyDifficulties), + new DirectListPanel(normal), new DirectListPanel(undownloadable), + new DirectListPanel(manyDifficulties), }, }, }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs index 883f0c5e3f..838347800f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online { typeof(HistoricalSection), typeof(PaginatedMostPlayedBeatmapContainer), - typeof(DrawableMostPlayedRow), + typeof(DrawableMostPlayedBeatmap), typeof(DrawableProfileRow) }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs new file mode 100644 index 0000000000..cc3b2ac68b --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.BeatmapSet; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Bindables; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneLeaderboardScopeSelector : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(LeaderboardScopeSelector), + }; + + public TestSceneLeaderboardScopeSelector() + { + Bindable scope = new Bindable(); + + Add(new LeaderboardScopeSelector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { BindTarget = scope } + }); + + AddStep(@"Select global", () => scope.Value = BeatmapLeaderboardScope.Global); + AddStep(@"Select country", () => scope.Value = BeatmapLeaderboardScope.Country); + AddStep(@"Select friend", () => scope.Value = BeatmapLeaderboardScope.Friend); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs index c344cb9598..1f5ba67e03 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs @@ -9,6 +9,8 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +using osu.Game.Users; +using osu.Framework.Bindables; namespace osu.Game.Tests.Visual.Online { @@ -23,18 +25,25 @@ namespace osu.Game.Tests.Visual.Online public TestSceneProfileRulesetSelector() { ProfileRulesetSelector selector; + Bindable user = new Bindable(); Child = selector = new ProfileRulesetSelector { Anchor = Anchor.Centre, Origin = Anchor.Centre, + User = { BindTarget = user } }; AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo)); AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo)); AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo)); AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo)); - AddStep("select default ruleset", selector.SelectDefaultRuleset); + + AddStep("User with osu as default", () => user.Value = new User { PlayMode = "osu" }); + AddStep("User with mania as default", () => user.Value = new User { PlayMode = "mania" }); + AddStep("User with taiko as default", () => user.Value = new User { PlayMode = "taiko" }); + AddStep("User with catch as default", () => user.Value = new User { PlayMode = "fruits" }); + AddStep("null user", () => user.Value = null); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs index 709e75ab13..c70cc4ae4e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs @@ -69,28 +69,22 @@ namespace osu.Game.Tests.Visual.Online } }); - AddStep("null user", () => graph.User.Value = null); + AddStep("null user", () => graph.Statistics.Value = null); AddStep("rank only", () => { - graph.User.Value = new User + graph.Statistics.Value = new UserStatistics { - Statistics = new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = 123456 }, - PP = 12345, - } + Ranks = new UserStatistics.UserRanks { Global = 123456 }, + PP = 12345, }; }); AddStep("with rank history", () => { - graph.User.Value = new User + graph.Statistics.Value = new UserStatistics { - Statistics = new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = 89000 }, - PP = 12345, - }, + Ranks = new UserStatistics.UserRanks { Global = 89000 }, + PP = 12345, RankHistory = new User.RankHistoryData { Data = data, @@ -100,13 +94,10 @@ namespace osu.Game.Tests.Visual.Online AddStep("with zero values", () => { - graph.User.Value = new User + graph.Statistics.Value = new UserStatistics { - Statistics = new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = 89000 }, - PP = 12345, - }, + Ranks = new UserStatistics.UserRanks { Global = 89000 }, + PP = 12345, RankHistory = new User.RankHistoryData { Data = dataWithZeros, @@ -116,13 +107,10 @@ namespace osu.Game.Tests.Visual.Online AddStep("small amount of data", () => { - graph.User.Value = new User + graph.Statistics.Value = new UserStatistics { - Statistics = new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = 12000 }, - PP = 12345, - }, + Ranks = new UserStatistics.UserRanks { Global = 12000 }, + PP = 12345, RankHistory = new User.RankHistoryData { Data = smallData, @@ -132,13 +120,10 @@ namespace osu.Game.Tests.Visual.Online AddStep("graph with edges", () => { - graph.User.Value = new User + graph.Statistics.Value = new UserStatistics { - Statistics = new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = 12000 }, - PP = 12345, - }, + Ranks = new UserStatistics.UserRanks { Global = 12000 }, + PP = 12345, RankHistory = new User.RankHistoryData { Data = edgyData, diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index c2376aa153..84c99d8c3a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -50,12 +50,12 @@ namespace osu.Game.Tests.Visual.Online { Current = 727, Progress = 69, - } - }, - RankHistory = new User.RankHistoryData - { - Mode = @"osu", - Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() + }, + RankHistory = new User.RankHistoryData + { + Mode = @"osu", + Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() + }, }, Badges = new[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs new file mode 100644 index 0000000000..d09a50b12c --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneUserProfilePreviousUsernames : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(PreviousUsernames) + }; + + [Resolved] + private IAPIProvider api { get; set; } + + private readonly Bindable user = new Bindable(); + + public TestSceneUserProfilePreviousUsernames() + { + Child = new PreviousUsernames + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = { BindTarget = user }, + }; + + User[] users = + { + new User { PreviousUsernames = new[] { "username1" } }, + new User { PreviousUsernames = new[] { "longusername", "longerusername" } }, + new User { PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } }, + new User { PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } }, + new User { PreviousUsernames = new string[0] }, + null + }; + + AddStep("single username", () => user.Value = users[0]); + AddStep("two usernames", () => user.Value = users[1]); + AddStep("three usernames", () => user.Value = users[2]); + AddStep("four usernames", () => user.Value = users[3]); + AddStep("no username", () => user.Value = users[4]); + AddStep("null user", () => user.Value = users[5]); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("online user (Angelsim)", () => + { + var request = new GetUserRequest(1777162); + request.Success += user => this.user.Value = user; + api.Queue(request); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserRequest.cs b/osu.Game.Tests/Visual/Online/TestSceneUserRequest.cs new file mode 100644 index 0000000000..18d6028cb8 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserRequest.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Users; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneUserRequest : OsuTestScene + { + [Resolved] + private IAPIProvider api { get; set; } + + private readonly Bindable user = new Bindable(); + private GetUserRequest request; + private readonly DimmedLoadingLayer loading; + + public TestSceneUserRequest() + { + Add(new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new UserTestContainer + { + User = { BindTarget = user } + }, + loading = new DimmedLoadingLayer + { + Alpha = 0 + } + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep(@"local user", () => getUser()); + AddStep(@"local user with taiko ruleset", () => getUser(ruleset: new TaikoRuleset().RulesetInfo)); + AddStep(@"cookiezi", () => getUser(124493)); + AddStep(@"cookiezi with mania ruleset", () => getUser(124493, new ManiaRuleset().RulesetInfo)); + } + + private void getUser(long? userId = null, RulesetInfo ruleset = null) + { + loading.Show(); + + request?.Cancel(); + request = new GetUserRequest(userId, ruleset); + request.Success += user => + { + this.user.Value = user; + loading.Hide(); + }; + api.Queue(request); + } + + private class UserTestContainer : FillFlowContainer + { + public readonly Bindable User = new Bindable(); + + public UserTestContainer() + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Vertical; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserUpdate, true); + } + + private void onUserUpdate(ValueChangedEvent user) + { + Clear(); + + AddRange(new Drawable[] + { + new SpriteText + { + Text = $@"Username: {user.NewValue?.Username}" + }, + new SpriteText + { + Text = $@"RankedScore: {user.NewValue?.Statistics.RankedScore}" + }, + }); + } + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 7c9b7c7815..6669ec7da3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -516,6 +516,7 @@ namespace osu.Game.Tests.Visual.SongSelect OnlineBeatmapID = b * 10, Path = $"extra{b}.osu", Version = $"Extra {b}", + Ruleset = rulesets.GetRuleset((b - 1) % 4), StarDifficulty = 2, BaseDifficulty = new BeatmapDifficulty { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index f3255814f2..263eada07c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -15,7 +15,7 @@ using osu.Framework.MathUtils; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Database; +using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -35,7 +35,6 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesets; private WorkingBeatmap defaultBeatmap; - private DatabaseContextFactory factory; public override IReadOnlyList RequiredTypes => new[] { @@ -74,32 +73,19 @@ namespace osu.Game.Tests.Visual.SongSelect private TestSongSelect songSelect; - protected override void Dispose(bool isDisposing) - { - factory.ResetDatabase(); - base.Dispose(isDisposing); - } - [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - factory = new DatabaseContextFactory(LocalStorage); - factory.ResetDatabase(); - - using (var usage = factory.Get()) - usage.Migrate(); - - factory.ResetDatabase(); - - using (var usage = factory.Get()) - usage.Migrate(); - - Dependencies.Cache(rulesets = new RulesetStore(factory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, defaultBeatmap = Beatmap.Default)); Beatmap.SetDefault(); + + Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); } + private OsuConfigManager config; + [SetUp] public virtual void SetUp() => Schedule(() => { @@ -130,13 +116,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); - AddStep(@"Sort by Artist", delegate { songSelect.FilterControl.Sort = SortMode.Artist; }); - AddStep(@"Sort by Title", delegate { songSelect.FilterControl.Sort = SortMode.Title; }); - AddStep(@"Sort by Author", delegate { songSelect.FilterControl.Sort = SortMode.Author; }); - AddStep(@"Sort by DateAdded", delegate { songSelect.FilterControl.Sort = SortMode.DateAdded; }); - AddStep(@"Sort by BPM", delegate { songSelect.FilterControl.Sort = SortMode.BPM; }); - AddStep(@"Sort by Length", delegate { songSelect.FilterControl.Sort = SortMode.Length; }); - AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; }); + var sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); + + AddStep(@"Sort by Artist", delegate { sortMode.Value = SortMode.Artist; }); + AddStep(@"Sort by Title", delegate { sortMode.Value = SortMode.Title; }); + AddStep(@"Sort by Author", delegate { sortMode.Value = SortMode.Author; }); + AddStep(@"Sort by DateAdded", delegate { sortMode.Value = SortMode.DateAdded; }); + AddStep(@"Sort by BPM", delegate { sortMode.Value = SortMode.BPM; }); + AddStep(@"Sort by Length", delegate { sortMode.Value = SortMode.Length; }); + AddStep(@"Sort by Difficulty", delegate { sortMode.Value = SortMode.Difficulty; }); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 28f0cc027e..94228e22f0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -22,30 +23,36 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneBeatSyncedContainer : OsuTestScene { - private readonly MusicController mc; + private readonly NowPlayingOverlay np; + + [Cached] + private MusicController musicController = new MusicController(); public TestSceneBeatSyncedContainer() { Clock = new FramedClock(); Clock.ProcessFrame(); - Add(new BeatContainer + AddRange(new Drawable[] { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }); - - Add(mc = new MusicController - { - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, + musicController, + new BeatContainer + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }, + np = new NowPlayingOverlay + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + } }); } protected override void LoadComplete() { base.LoadComplete(); - mc.ToggleVisibility(); + np.ToggleVisibility(); } private class BeatContainer : BeatSyncedContainer diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs index 23d9112b25..e95f4c09c6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs @@ -249,7 +249,7 @@ namespace osu.Game.Tests.Visual.UserInterface Size = new Vector2(50); Masking = true; - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; Alpha = 0.5f; Child = new Box { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs index 7e6cf1285e..f787754aa4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Menu; @@ -12,7 +13,13 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneHoldToConfirmOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(ExitConfirmOverlay) }; + protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ExitConfirmOverlay), + typeof(HoldToConfirmContainer), + }; public TestSceneHoldToConfirmOverlay() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 80408ab43b..be50200e1c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -82,14 +82,13 @@ namespace osu.Game.Tests.Visual.UserInterface var easierMods = instance.GetModsFor(ModType.DifficultyReduction); var harderMods = instance.GetModsFor(ModType.DifficultyIncrease); - var assistMods = instance.GetModsFor(ModType.Automation); var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); - var autoPilotMod = assistMods.FirstOrDefault(m => m is OsuModAutopilot); + var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); @@ -101,7 +100,7 @@ namespace osu.Game.Tests.Visual.UserInterface testMultiplierTextColour(noFailMod, modSelect.LowMultiplierColour); testMultiplierTextColour(hiddenMod, modSelect.HighMultiplierColour); - testUnimplementedMod(autoPilotMod); + testUnimplementedMod(spunOutMod); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs similarity index 58% rename from osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index ab2ca47100..e3daa9c279 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMusicController.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -2,6 +2,7 @@ // 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.Timing; using osu.Game.Overlays; @@ -9,22 +10,27 @@ using osu.Game.Overlays; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneMusicController : OsuTestScene + public class TestSceneNowPlayingOverlay : OsuTestScene { - public TestSceneMusicController() + [Cached] + private MusicController musicController = new MusicController(); + + public TestSceneNowPlayingOverlay() { Clock = new FramedClock(); - var mc = new MusicController + var np = new NowPlayingOverlay { Origin = Anchor.Centre, Anchor = Anchor.Centre }; - Add(mc); - AddStep(@"show", () => mc.Show()); + Add(musicController); + Add(np); + + AddStep(@"show", () => np.Show()); AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); - AddStep(@"show", () => mc.Hide()); + AddStep(@"show", () => np.Hide()); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs index d900526c07..45720548c8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics; using osu.Game.Overlays; +using osu.Game.Overlays.OSD; namespace osu.Game.Tests.Visual.UserInterface { @@ -22,6 +23,12 @@ namespace osu.Game.Tests.Visual.UserInterface osd.BeginTracking(this, config); Add(osd); + AddStep("Display empty osd toast", () => osd.Display(new EmptyToast())); + AddAssert("Toast width is 240", () => osd.Child.Width == 240); + + AddStep("Display toast with lengthy text", () => osd.Display(new LengthyToast())); + AddAssert("Toast width is greater than 240", () => osd.Child.Width > 240); + AddRepeatStep("Change toggle (no bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingNoKeybind), 2); AddRepeatStep("Change toggle (with bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingWithKeybind), 2); AddRepeatStep("Change enum (no bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingNoKeybind), 3); @@ -86,6 +93,22 @@ namespace osu.Game.Tests.Visual.UserInterface Setting4 } + private class EmptyToast : Toast + { + public EmptyToast() + : base("", "", "") + { + } + } + + private class LengthyToast : Toast + { + public LengthyToast() + : base("Toast with a very very very long text", "A very very very very very very long text also", "A very very very very very long shortcut") + { + } + } + private class TestOnScreenDisplay : OnScreenDisplay { protected override void DisplayTemporarily(Drawable toDisplay) => toDisplay.FadeIn().ResizeHeightTo(110); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 659f5415c3..4a9d88f3a6 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs index f329623703..e65b708fea 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Ladder.Components; namespace osu.Game.Tournament.Tests.Components { - public class TestSceneDrawableTournamentMatch : OsuTestScene + public class TestSceneDrawableTournamentMatch : TournamentTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs index b49341d0d1..dae0721023 100644 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ b/osu.Game.Tournament.Tests/LadderTestScene.cs @@ -1,13 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; using osu.Framework.Allocation; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests { - public abstract class LadderTestScene : OsuTestScene + [TestFixture] + public abstract class LadderTestScene : TournamentTestScene { [Resolved] protected LadderInfo Ladder { get; private set; } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 201736f38a..9de00818a5 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -2,13 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneGameplayScreen : OsuTestScene + public class TestSceneGameplayScreen : TournamentTestScene { [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index f3e65919eb..2277302e98 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs @@ -2,12 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Screens.Schedule; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneScheduleScreen : OsuTestScene + public class TestSceneScheduleScreen : TournamentTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs index edf1477b06..8c43e25416 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneShowcaseScreen.cs @@ -2,12 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Screens.Showcase; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneShowcaseScreen : OsuTestScene + public class TestSceneShowcaseScreen : TournamentTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs index 378614343a..4d134ce4af 100644 --- a/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs +++ b/osu.Game.Tournament.Tests/TestSceneTournamentSceneManager.cs @@ -3,11 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Platform; -using osu.Game.Tests.Visual; namespace osu.Game.Tournament.Tests { - public class TestSceneTournamentSceneManager : OsuTestScene + public class TestSceneTournamentSceneManager : TournamentTestScene { [BackgroundDependencyLoader] private void load(Storage storage) diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs new file mode 100644 index 0000000000..18ac3230da --- /dev/null +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Testing; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tournament.Tests +{ + public abstract class TournamentTestScene : OsuTestScene + { + protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner(); + + public class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner + { + private TestSceneTestRunner.TestRunner runner; + + protected override void LoadAsyncComplete() + { + // this has to be run here rather than LoadComplete because + // TestScene.cs is checking the IsLoaded state (on another thread) and expects + // the runner to be loaded at that point. + Add(runner = new TestSceneTestRunner.TestRunner()); + } + + public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); + } + } +} diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index dad2fe0877..2a8bd393da 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index d5e28c1e3e..f6c1be0e36 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -125,7 +125,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = Color4.Gray, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0, }, }); diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 67531ce5d3..83a41a662f 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tournament.Screens.Ladder }; } - private Cached layout = new Cached(); + private readonly Cached layout = new Cached(); protected override void Update() { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 46d4cfa98c..d32c0d6156 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Screens.MapPool mapFlows = new FillFlowContainer> { Y = 100, - Spacing = new Vector2(10, 20), - Padding = new MarginPadding(50), + Spacing = new Vector2(10, 10), + Padding = new MarginPadding(25), Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, }, @@ -218,7 +218,7 @@ namespace osu.Game.Tournament.Screens.MapPool { mapFlows.Add(currentFlow = new FillFlowContainer { - Spacing = new Vector2(10, 20), + Spacing = new Vector2(10, 5), Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index d809dfc994..20928499bf 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Screens.Showcase [BackgroundDependencyLoader] private void load() { - AddInternal(new TournamentLogo()); + AddInternal(new TournamentLogo(false)); } } } diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index efe4ee92fc..a0216c5db3 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Screens.TeamWin RelativeSizeAxes = Axes.Both, Loop = true, }, - new TournamentLogo + new TournamentLogo(false) { Y = 40, }, diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 06fb52da77..dbfa70704b 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -9,15 +9,20 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; +using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tournament @@ -35,6 +40,8 @@ namespace osu.Game.Tournament private Bindable windowSize; private FileBasedIPC ipc; + private Drawable heightWarning; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -53,6 +60,12 @@ namespace osu.Game.Tournament this.storage = storage; windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); + windowSize.BindValueChanged(size => ScheduleAfterChildren(() => + { + var minWidth = (int)(size.NewValue.Height / 9f * 16 + 400); + + heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; + }), true); readBracket(); @@ -61,16 +74,43 @@ namespace osu.Game.Tournament dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); - Add(new OsuButton + AddRange(new[] { - Text = "Save Changes", - Width = 140, - Height = 50, - Depth = float.MinValue, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Padding = new MarginPadding(10), - Action = SaveChanges, + new OsuButton + { + Text = "Save Changes", + Width = 140, + Height = 50, + Depth = float.MinValue, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Padding = new MarginPadding(10), + Action = SaveChanges, + }, + heightWarning = new Container + { + Masking = true, + CornerRadius = 5, + Depth = float.MinValue, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Red, + RelativeSizeAxes = Axes.Both, + }, + new SpriteText + { + Text = "Please make the window wider", + Font = OsuFont.Default.With(weight: "bold"), + Colour = Color4.White, + Padding = new MarginPadding(20) + } + } + }, }); } @@ -195,18 +235,6 @@ namespace osu.Game.Tournament base.LoadComplete(); } - protected override void Update() - { - base.Update(); - var minWidth = (int)(windowSize.Value.Height / 9f * 16 + 400); - - if (windowSize.Value.Width < minWidth) - { - // todo: can be removed after ppy/osu-framework#1975 - windowSize.Value = Host.Window.ClientSize = new Size(minWidth, windowSize.Value.Height); - } - } - protected virtual void SaveChanges() { foreach (var r in ladder.Rounds) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 4ebeee40bf..a09a1bb9cb 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -60,5 +60,7 @@ namespace osu.Game.Beatmaps public class Beatmap : Beatmap { public new Beatmap Clone() => (Beatmap)base.Clone(); + + public override string ToString() => BeatmapInfo?.ToString() ?? base.ToString(); } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8042f6b4b9..198046df4f 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -129,6 +129,23 @@ namespace osu.Game.Beatmaps /// public List Scores { get; set; } + [JsonIgnore] + public DifficultyRating DifficultyRating + { + get + { + var rating = StarDifficulty; + + if (rating < 2.0) return DifficultyRating.Easy; + if (rating < 2.7) return DifficultyRating.Normal; + if (rating < 4.0) return DifficultyRating.Hard; + if (rating < 5.3) return DifficultyRating.Insane; + if (rating < 6.5) return DifficultyRating.Expert; + + return DifficultyRating.ExpertPlus; + } + } + public override string ToString() => $"{Metadata} [{Version}]".Trim(); public bool Equals(BeatmapInfo other) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4f67139706..166ba5111c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -181,19 +181,18 @@ namespace osu.Game.Beatmaps lock (workingCache) { - var cached = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); - if (cached != null) - return cached; + if (working == null) + { + if (beatmapInfo.Metadata == null) + beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; - if (beatmapInfo.Metadata == null) - beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; - - WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager); + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, + new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager)); + } previous?.TransferTo(working); - workingCache.Add(working); - return working; } } @@ -387,7 +386,7 @@ namespace osu.Game.Beatmaps beatmap.OnlineBeatmapID = res.OnlineBeatmapID; }; - req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap}", e); }; + req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); }; // intentionally blocking to limit web request concurrency req.Perform(api); diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 5657b8fb8a..2d8a0b1249 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -138,19 +138,15 @@ namespace osu.Game.Beatmaps protected override Skin GetSkin() { - Skin skin; - try { - skin = new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager); + return new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager); } catch (Exception e) { Logger.Error(e, "Skin failed to load"); - skin = new DefaultSkin(); + return null; } - - return skin; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index 4cb50b59fb..decf2d10db 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -67,6 +67,11 @@ namespace osu.Game.Beatmaps /// public int FavouriteCount { get; set; } + /// + /// Whether this beatmap set has been favourited by the current user. + /// + public bool HasFavourited { get; set; } + /// /// The availability of this beatmap set. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index e5815a3f3b..ccb8a92b3a 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -14,6 +14,8 @@ namespace osu.Game.Beatmaps.ControlPoints /// public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple; + public const double DEFAULT_BEAT_LENGTH = 1000; + /// /// The beat length at this control point. /// @@ -23,7 +25,7 @@ namespace osu.Game.Beatmaps.ControlPoints set => beatLength = MathHelper.Clamp(value, 6, 60000); } - private double beatLength = 1000; + private double beatLength = DEFAULT_BEAT_LENGTH; public bool Equals(TimingControlPoint other) => base.Equals(other) diff --git a/osu.Game/Beatmaps/DifficultyRating.cs b/osu.Game/Beatmaps/DifficultyRating.cs new file mode 100644 index 0000000000..f0ee0ad705 --- /dev/null +++ b/osu.Game/Beatmaps/DifficultyRating.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + public enum DifficultyRating + { + Easy, + Normal, + Hard, + Insane, + Expert, + ExpertPlus + } +} diff --git a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs deleted file mode 100644 index 26ffcca1ec..0000000000 --- a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Beatmaps.Drawables -{ - public abstract class DifficultyColouredContainer : Container, IHasAccentColour - { - public Color4 AccentColour { get; set; } - - private readonly BeatmapInfo beatmap; - private OsuColour palette; - - protected DifficultyColouredContainer(BeatmapInfo beatmap) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load(OsuColour palette) - { - if (palette == null) - throw new ArgumentNullException(nameof(palette)); - - this.palette = palette; - AccentColour = getColour(beatmap); - } - - private enum DifficultyRating - { - Easy, - Normal, - Hard, - Insane, - Expert, - ExpertPlus - } - - private DifficultyRating getDifficultyRating(BeatmapInfo beatmap) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - - var rating = beatmap.StarDifficulty; - - if (rating < 2.0) return DifficultyRating.Easy; - if (rating < 2.7) return DifficultyRating.Normal; - if (rating < 4.0) return DifficultyRating.Hard; - if (rating < 5.3) return DifficultyRating.Insane; - if (rating < 6.5) return DifficultyRating.Expert; - - return DifficultyRating.ExpertPlus; - } - - private Color4 getColour(BeatmapInfo beatmap) - { - switch (getDifficultyRating(beatmap)) - { - case DifficultyRating.Easy: - return palette.Green; - - default: - case DifficultyRating.Normal: - return palette.Blue; - - case DifficultyRating.Hard: - return palette.Yellow; - - case DifficultyRating.Insane: - return palette.Pink; - - case DifficultyRating.Expert: - return palette.Purple; - - case DifficultyRating.ExpertPlus: - return palette.Gray0; - } - } - } -} diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 0a0ad28fdf..81f517dd86 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -6,35 +6,58 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - public class DifficultyIcon : DifficultyColouredContainer + public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip { + private readonly BeatmapInfo beatmap; private readonly RulesetInfo ruleset; - public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null) - : base(beatmap) + private readonly Container iconContainer; + + /// + /// Size of this difficulty icon. + /// + public new Vector2 Size { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - - this.ruleset = ruleset ?? beatmap.Ruleset; - - Size = new Vector2(20); + get => iconContainer.Size; + set => iconContainer.Size = value; } - [BackgroundDependencyLoader] - private void load() + public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null, bool shouldShowTooltip = true) { - Children = new Drawable[] + this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); + + this.ruleset = ruleset ?? beatmap.Ruleset; + if (shouldShowTooltip) + TooltipContent = beatmap; + + AutoSizeAxes = Axes.Both; + + InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; + } + + public string TooltipText { get; set; } + + public ITooltip GetCustomTooltip() => new DifficultyIconTooltip(); + + public object TooltipContent { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + iconContainer.Children = new Drawable[] { new CircularContainer { @@ -52,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = AccentColour, + Colour = colours.ForDifficultyRating(beatmap.DifficultyRating), }, }, new ConstrainedIconContainer @@ -65,5 +88,96 @@ namespace osu.Game.Beatmaps.Drawables } }; } + + private class DifficultyIconTooltip : VisibilityContainer, ITooltip + { + private readonly OsuSpriteText difficultyName, starRating; + private readonly Box background; + + private readonly FillFlowContainer difficultyFlow; + + public DifficultyIconTooltip() + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 5; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 200, + AutoSizeEasing = Easing.OutQuint, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + difficultyName = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), + }, + difficultyFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + starRating = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Left = 4 }, + Icon = FontAwesome.Solid.Star, + Size = new Vector2(12), + }, + } + } + } + } + }; + } + + private OsuColour colours; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + this.colours = colours; + background.Colour = colours.Gray3; + } + + public bool SetContent(object content) + { + if (!(content is BeatmapInfo beatmap)) + return false; + + difficultyName.Text = beatmap.Version; + starRating.Text = $"{beatmap.StarDifficulty:0.##}"; + difficultyFlow.Colour = colours.ForDifficultyRating(beatmap.DifficultyRating); + + return true; + } + + public void Move(Vector2 pos) => Position = pos; + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + } } } diff --git a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs new file mode 100644 index 0000000000..fbad113caa --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osuTK.Graphics; + +namespace osu.Game.Beatmaps.Drawables +{ + /// + /// A difficulty icon that contains a counter on the right-side of it. + /// + /// + /// Used in cases when there are too many difficulty icons to show. + /// + public class GroupedDifficultyIcon : DifficultyIcon + { + public GroupedDifficultyIcon(List beatmaps, RulesetInfo ruleset, Color4 counterColour) + : base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, false) + { + AddInternal(new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Padding = new MarginPadding { Left = Size.X }, + Margin = new MarginPadding { Left = 2, Right = 5 }, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Text = beatmaps.Count.ToString(), + Colour = counterColour, + }); + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3cd425ea44..02d969b571 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Linq; using osu.Framework.IO.File; -using osu.Framework.Logging; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; @@ -290,8 +289,9 @@ namespace osu.Game.Beatmaps.Formats string[] split = line.Split(','); EventType type; + if (!Enum.TryParse(split[0], out type)) - throw new InvalidDataException($@"Unknown event type {split[0]}"); + throw new InvalidDataException($@"Unknown event type: {split[0]}"); switch (type) { @@ -319,90 +319,79 @@ namespace osu.Game.Beatmaps.Formats private void handleTimingPoint(string line) { - try + string[] split = line.Split(','); + + double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); + double beatLength = Parsing.ParseDouble(split[1].Trim()); + double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; + + TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; + if (split.Length >= 3) + timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]); + + LegacySampleBank sampleSet = defaultSampleBank; + if (split.Length >= 4) + sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]); + + int customSampleBank = 0; + if (split.Length >= 5) + customSampleBank = Parsing.ParseInt(split[4]); + + int sampleVolume = defaultSampleVolume; + if (split.Length >= 6) + sampleVolume = Parsing.ParseInt(split[5]); + + bool timingChange = true; + if (split.Length >= 7) + timingChange = split[6][0] == '1'; + + bool kiaiMode = false; + bool omitFirstBarSignature = false; + + if (split.Length >= 8) { - string[] split = line.Split(','); - - double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); - double beatLength = Parsing.ParseDouble(split[1].Trim()); - double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; - - TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; - if (split.Length >= 3) - timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]); - - LegacySampleBank sampleSet = defaultSampleBank; - if (split.Length >= 4) - sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]); - - int customSampleBank = 0; - if (split.Length >= 5) - customSampleBank = Parsing.ParseInt(split[4]); - - int sampleVolume = defaultSampleVolume; - if (split.Length >= 6) - sampleVolume = Parsing.ParseInt(split[5]); - - bool timingChange = true; - if (split.Length >= 7) - timingChange = split[6][0] == '1'; - - bool kiaiMode = false; - bool omitFirstBarSignature = false; - - if (split.Length >= 8) - { - EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine); - } - - string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); - if (stringSampleSet == @"none") - stringSampleSet = @"normal"; - - if (timingChange) - { - var controlPoint = CreateTimingControlPoint(); - controlPoint.Time = time; - controlPoint.BeatLength = beatLength; - controlPoint.TimeSignature = timeSignature; - - handleTimingControlPoint(controlPoint); - } - - handleDifficultyControlPoint(new DifficultyControlPoint - { - Time = time, - SpeedMultiplier = speedMultiplier, - AutoGenerated = timingChange - }); - - handleEffectControlPoint(new EffectControlPoint - { - Time = time, - KiaiMode = kiaiMode, - OmitFirstBarLine = omitFirstBarSignature, - AutoGenerated = timingChange - }); - - handleSampleControlPoint(new LegacySampleControlPoint - { - Time = time, - SampleBank = stringSampleSet, - SampleVolume = sampleVolume, - CustomSampleBank = customSampleBank, - AutoGenerated = timingChange - }); + EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]); + kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine); } - catch (FormatException) + + string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); + if (stringSampleSet == @"none") + stringSampleSet = @"normal"; + + if (timingChange) { - Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); + var controlPoint = CreateTimingControlPoint(); + controlPoint.Time = time; + controlPoint.BeatLength = beatLength; + controlPoint.TimeSignature = timeSignature; + + handleTimingControlPoint(controlPoint); } - catch (OverflowException) + + handleDifficultyControlPoint(new DifficultyControlPoint { - Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); - } + Time = time, + SpeedMultiplier = speedMultiplier, + AutoGenerated = timingChange + }); + + handleEffectControlPoint(new EffectControlPoint + { + Time = time, + KiaiMode = kiaiMode, + OmitFirstBarLine = omitFirstBarSignature, + AutoGenerated = timingChange + }); + + handleSampleControlPoint(new LegacySampleControlPoint + { + Time = time, + SampleBank = stringSampleSet, + SampleVolume = sampleVolume, + CustomSampleBank = customSampleBank, + AutoGenerated = timingChange + }); } private void handleTimingControlPoint(TimingControlPoint newPoint) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 7999c82761..9a8197ad82 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps.Formats { if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) { - Logger.Log($"Unknown section \"{line}\" in {output}"); + Logger.Log($"Unknown section \"{line}\" in \"{output}\""); section = Section.None; } @@ -49,7 +49,7 @@ namespace osu.Game.Beatmaps.Formats } catch (Exception e) { - Logger.Error(e, $"Failed to process line \"{line}\" into {output}"); + Logger.Log($"Failed to process line \"{line}\" into \"{output}\": {e.Message}", LoggingTarget.Runtime, LogLevel.Important); } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index 540f616ea9..2c493254e0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint { - public override double BeatLength { get; set; } = 1000; + public override double BeatLength { get; set; } = DEFAULT_BEAT_LENGTH; } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index f6e2bf6966..14c6ea5c8e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -82,8 +82,9 @@ namespace osu.Game.Beatmaps.Formats storyboardSprite = null; EventType type; + if (!Enum.TryParse(split[0], out type)) - throw new InvalidDataException($@"Unknown event type {split[0]}"); + throw new InvalidDataException($@"Unknown event type: {split[0]}"); switch (type) { @@ -120,7 +121,7 @@ namespace osu.Game.Beatmaps.Formats var layer = parseLayer(split[2]); var path = cleanFilename(split[3]); var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; - storyboard.GetLayer(layer).Add(new StoryboardSample(path, time, volume)); + storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume)); break; } } @@ -245,7 +246,7 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case "A": - timelineGroup?.BlendingMode.Add(easing, startTime, endTime, BlendingMode.Additive, startTime == endTime ? BlendingMode.Additive : BlendingMode.Inherit); + timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); break; case "H": diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 8e53c24e7b..583e950e49 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -9,7 +9,7 @@ namespace osu.Game.Beatmaps.Legacy public enum LegacyMods { None = 0, - NoFail = 1 << 0, + NoFail = 1, Easy = 1 << 1, TouchDevice = 1 << 2, Hidden = 1 << 3, diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index 856a5fefd4..5d79c7a86b 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Screens.Play; + namespace osu.Game.Beatmaps.Timing { public class BreakPeriod @@ -35,6 +37,6 @@ namespace osu.Game.Beatmaps.Timing /// /// The time to check in milliseconds. /// Whether the time falls within this . - public bool Contains(double time) => time >= StartTime && time <= EndTime; + public bool Contains(double time) => time >= StartTime && time <= EndTime - BreakOverlay.BREAK_FADE_DURATION; } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 37aa0024da..8605caa5fe 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -89,6 +89,14 @@ namespace osu.Game.Beatmaps return path; } + /// + /// Creates a to convert a for a specified . + /// + /// The to be converted. + /// The for which should be converted. + /// The applicable . + protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); + /// /// Constructs a playable from using the applicable converters for a specific . /// @@ -104,7 +112,7 @@ namespace osu.Game.Beatmaps { var rulesetInstance = ruleset.CreateInstance(); - IBeatmapConverter converter = rulesetInstance.CreateBeatmapConverter(Beatmap); + IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); // Check if the beatmap can be converted if (!converter.CanConvert) @@ -141,6 +149,9 @@ namespace osu.Game.Beatmaps processor?.PostProcess(); + foreach (var mod in mods.OfType()) + mod.ApplyToBeatmap(converted); + return converted; } @@ -247,7 +258,7 @@ namespace osu.Game.Beatmaps // cancelling the beatmap load is safe for now since the retrieval is a synchronous // operation. if we add an async retrieval method this may need to be reconsidered. - beatmapCancellation.Cancel(); + beatmapCancellation?.Cancel(); total_count.Value--; } diff --git a/osu.Game/Configuration/IntroSequence.cs b/osu.Game/Configuration/IntroSequence.cs new file mode 100644 index 0000000000..1eb953be36 --- /dev/null +++ b/osu.Game/Configuration/IntroSequence.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Configuration +{ + public enum IntroSequence + { + Circles, + Triangles + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1da7c7ec1d..b13e115387 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -8,6 +8,7 @@ using osu.Framework.Platform; using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Configuration { @@ -25,6 +26,9 @@ namespace osu.Game.Configuration Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); Set(OsuSetting.DisplayStarsMaximum, 10.0, 0, 10, 0.1); + Set(OsuSetting.SongSelectGroupingMode, GroupMode.All); + Set(OsuSetting.SongSelectSortingMode, SortMode.Title); + Set(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1); @@ -105,6 +109,8 @@ namespace osu.Game.Configuration Set(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f); Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); + + Set(OsuSetting.IntroSequence, IntroSequence.Triangles); } public OsuConfigManager(Storage storage) @@ -148,6 +154,8 @@ namespace osu.Game.Configuration SaveUsername, DisplayStarsMinimum, DisplayStarsMaximum, + SongSelectGroupingMode, + SongSelectSortingMode, RandomSelectAlgorithm, ShowFpsDisplay, ChatDisplayHeight, @@ -167,6 +175,7 @@ namespace osu.Game.Configuration ScalingPositionY, ScalingSizeX, ScalingSizeY, - UIScale + UIScale, + IntroSequence } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ed65bdc069..52d3f013ce 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -31,10 +31,21 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ArchiveModelManager, ICanAcceptFiles, IModelManager + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : INamedFileInfo, new() { + private const int import_queue_request_concurrency = 1; + + /// + /// A singleton scheduler shared by all . + /// + /// + /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly. + /// It is mainly being used as a queue mechanism for large imports. + /// + private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); + /// /// Set an endpoint for notifications to be posted to. /// @@ -253,7 +264,7 @@ namespace osu.Game.Database using (Stream s = reader.GetStream(file)) s.CopyTo(hashable); - return hashable.ComputeSHA2Hash(); + return hashable.Length > 0 ? hashable.ComputeSHA2Hash() : null; } /// @@ -336,7 +347,7 @@ namespace osu.Game.Database flushEvents(true); return item; - }, cancellationToken, TaskCreationOptions.HideScheduler, IMPORT_SCHEDULER).Unwrap(); + }, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap(); /// /// Perform an update of the specified item. @@ -646,18 +657,4 @@ namespace osu.Game.Database #endregion } - - public abstract class ArchiveModelManager - { - private const int import_queue_request_concurrency = 1; - - /// - /// A singleton scheduler shared by all . - /// - /// - /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly. - /// It is mainly being used as a queue mechanism for large imports. - /// - protected static readonly ThreadedTaskScheduler IMPORT_SCHEDULER = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); - } } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 538ec41b3d..ea3318598f 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -41,6 +41,9 @@ namespace osu.Game.Database { // required to initialise native SQLite libraries on some platforms. SQLitePCL.Batteries_V2.Init(); + + // https://github.com/aspnet/EntityFrameworkCore/issues/9994#issuecomment-508588678 + SQLitePCL.raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/); } /// diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index 526b3da8a6..d13475189d 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -16,7 +16,7 @@ namespace osu.Game.Graphics.Backgrounds /// public class Background : CompositeDrawable { - public Sprite Sprite; + public readonly Sprite Sprite; private readonly string textureName; @@ -51,7 +51,7 @@ namespace osu.Game.Graphics.Backgrounds /// A to which further transforms can be added. public void BlurTo(Vector2 newBlurSigma, double duration = 0, Easing easing = Easing.None) { - if (bufferedContainer == null) + if (bufferedContainer == null && newBlurSigma != Vector2.Zero) { RemoveInternal(Sprite); @@ -63,7 +63,7 @@ namespace osu.Game.Graphics.Backgrounds }); } - bufferedContainer.BlurTo(newBlurSigma, duration, easing); + bufferedContainer?.BlurTo(newBlurSigma, duration, easing); } } } diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 29113e0e2f..dffa0c4fd5 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -8,7 +8,6 @@ using osuTK.Graphics; using System; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; -using osuTK.Graphics.ES30; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; @@ -137,11 +136,13 @@ namespace osu.Game.Graphics.Backgrounds } } + protected int AimCount; + private void addTriangles(bool randomY) { - int aimTriangleCount = (int)(DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio); + AimCount = (int)(DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio); - for (int i = 0; i < aimTriangleCount - parts.Count; i++) + for (int i = 0; i < AimCount - parts.Count; i++) parts.Add(createTriangle(randomY)); } @@ -190,7 +191,7 @@ namespace osu.Game.Graphics.Backgrounds private readonly List parts = new List(); private Vector2 size; - private readonly LinearBatch vertexBatch = new LinearBatch(100 * 3, 10, PrimitiveType.Triangles); + private QuadBatch vertexBatch; public TrianglesDrawNode(Triangles source) : base(source) @@ -213,6 +214,12 @@ namespace osu.Game.Graphics.Backgrounds { base.Draw(vertexAction); + if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) + { + vertexBatch?.Dispose(); + vertexBatch = new QuadBatch(Source.AimCount, 1); + } + shader.Bind(); Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy; @@ -246,7 +253,7 @@ namespace osu.Game.Graphics.Backgrounds { base.Dispose(isDisposing); - vertexBatch.Dispose(); + vertexBatch?.Dispose(); } } diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index cda5e150de..773265d19b 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -12,9 +12,11 @@ namespace osu.Game.Graphics.Containers { public Action Action; - private const int activate_delay = 400; + private const int default_activation_delay = 200; private const int fadeout_delay = 200; + private readonly double activationDelay; + private bool fired; private bool confirming; @@ -25,13 +27,22 @@ namespace osu.Game.Graphics.Containers public Bindable Progress = new BindableDouble(); + /// + /// Create a new instance. + /// + /// The time requried before an action is confirmed. + protected HoldToConfirmContainer(double activationDelay = default_activation_delay) + { + this.activationDelay = activationDelay; + } + protected void BeginConfirm() { if (confirming || (!AllowMultipleFires && fired)) return; confirming = true; - this.TransformBindableTo(Progress, 1, activate_delay * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); + this.TransformBindableTo(Progress, 1, activationDelay * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); } protected virtual void Confirm() diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 5606328575..9c948d6f90 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -15,6 +15,7 @@ using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { + [Cached(typeof(IPreviewTrackOwner))] public abstract class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler { private SampleChannel samplePopIn; @@ -38,13 +39,6 @@ namespace osu.Game.Graphics.Containers protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(this); - return dependencies; - } - [BackgroundDependencyLoader(true)] private void load(AudioManager audio) { @@ -68,15 +62,23 @@ namespace osu.Game.Graphics.Containers protected override bool OnClick(ClickEvent e) { - if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) - { - Hide(); - return true; - } + closeIfOutside(e); return base.OnClick(e); } + protected override bool OnDragEnd(DragEndEvent e) + { + closeIfOutside(e); + return base.OnDragEnd(e); + } + + private void closeIfOutside(MouseEvent e) + { + if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) + Hide(); + } + public virtual bool OnPressed(GlobalAction action) { switch (action) diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index 53092ddc9e..2721ce55dc 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -27,11 +27,12 @@ namespace osu.Game.Graphics.Containers private bool shouldPerformRightMouseScroll(MouseButtonEvent e) => RightMouseScrollbar && e.Button == MouseButton.Right; - private void scrollToRelative(float value) => ScrollTo(Clamp((value - Scrollbar.DrawSize[ScrollDim] / 2) / Scrollbar.Size[ScrollDim]), true, DistanceDecayOnRightMouseScrollbar); + private void scrollFromMouseEvent(MouseEvent e) => + ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar); - private bool mouseScrollBarDragging; + private bool rightMouseDragging; - protected override bool IsDragging => base.IsDragging || mouseScrollBarDragging; + protected override bool IsDragging => base.IsDragging || rightMouseDragging; public OsuScrollContainer(Direction scrollDirection = Direction.Vertical) : base(scrollDirection) @@ -42,7 +43,7 @@ namespace osu.Game.Graphics.Containers { if (shouldPerformRightMouseScroll(e)) { - scrollToRelative(e.MousePosition[ScrollDim]); + scrollFromMouseEvent(e); return true; } @@ -51,9 +52,9 @@ namespace osu.Game.Graphics.Containers protected override bool OnDrag(DragEvent e) { - if (mouseScrollBarDragging) + if (rightMouseDragging) { - scrollToRelative(e.MousePosition[ScrollDim]); + scrollFromMouseEvent(e); return true; } @@ -64,7 +65,7 @@ namespace osu.Game.Graphics.Containers { if (shouldPerformRightMouseScroll(e)) { - mouseScrollBarDragging = true; + rightMouseDragging = true; return true; } @@ -73,9 +74,9 @@ namespace osu.Game.Graphics.Containers protected override bool OnDragEnd(DragEndEvent e) { - if (mouseScrollBarDragging) + if (rightMouseDragging) { - mouseScrollBarDragging = false; + rightMouseDragging = false; return true; } @@ -97,7 +98,7 @@ namespace osu.Game.Graphics.Containers public OsuScrollbar(Direction scrollDir) : base(scrollDir) { - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; CornerRadius = 5; diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index fe9eb7baf4..2b7635cc88 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -1,31 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Screens.Play; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Graphics.Containers { /// /// A container that applies user-configured visual settings to its contents. - /// This container specifies behavior that applies to both Storyboards and Backgrounds. /// - public class UserDimContainer : Container + public abstract class UserDimContainer : Container { - private const float background_fade_duration = 800; + protected const float BACKGROUND_FADE_DURATION = 800; /// /// Whether or not user-configured dim levels should be applied to the container. /// - public readonly Bindable EnableUserDim = new Bindable(); + public readonly Bindable EnableUserDim = new Bindable(true); /// /// Whether or not the storyboard loaded should completely hide the background behind it. @@ -33,103 +27,60 @@ namespace osu.Game.Graphics.Containers public readonly Bindable StoryboardReplacesBackground = new Bindable(); /// - /// The amount of blur to be applied to the background in addition to user-specified blur. + /// Whether the content of this container is currently being displayed. /// - /// - /// Used in contexts where there can potentially be both user and screen-specified blurring occuring at the same time, such as in - /// - public readonly Bindable BlurAmount = new Bindable(); + public bool ContentDisplayed { get; private set; } - private Bindable userDimLevel { get; set; } + protected Bindable UserDimLevel { get; private set; } - private Bindable userBlurLevel { get; set; } + protected Bindable ShowStoryboard { get; private set; } - private Bindable showStoryboard { get; set; } + protected double DimLevel => EnableUserDim.Value ? UserDimLevel.Value : 0; - protected Container DimContainer { get; } + protected override Container Content => dimContent; - protected override Container Content => DimContainer; - - private readonly bool isStoryboard; - - /// - /// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs. - /// - private Vector2 blurTarget => EnableUserDim.Value - ? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * 25) - : new Vector2(BlurAmount.Value); + private Container dimContent { get; } /// /// Creates a new . /// - /// Whether or not this instance contains a storyboard. - /// - /// While both backgrounds and storyboards allow user dim levels to be applied, storyboards can be toggled via - /// and can cause backgrounds to become hidden via . Storyboards are also currently unable to be blurred. - /// - /// - public UserDimContainer(bool isStoryboard = false) + protected UserDimContainer() { - this.isStoryboard = isStoryboard; - AddInternal(DimContainer = new Container { RelativeSizeAxes = Axes.Both }); - } - - private Background background; - - public Background Background - { - get => background; - set - { - base.Add(background = value); - background.BlurTo(blurTarget, 0, Easing.OutQuint); - } - } - - public override void Add(Drawable drawable) - { - if (drawable is Background) - throw new InvalidOperationException($"Use {nameof(Background)} to set a background."); - - base.Add(drawable); + AddInternal(dimContent = new Container { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - userDimLevel = config.GetBindable(OsuSetting.DimLevel); - userBlurLevel = config.GetBindable(OsuSetting.BlurLevel); - showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); + UserDimLevel = config.GetBindable(OsuSetting.DimLevel); + ShowStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); - EnableUserDim.ValueChanged += _ => updateVisuals(); - userDimLevel.ValueChanged += _ => updateVisuals(); - userBlurLevel.ValueChanged += _ => updateVisuals(); - showStoryboard.ValueChanged += _ => updateVisuals(); - StoryboardReplacesBackground.ValueChanged += _ => updateVisuals(); - BlurAmount.ValueChanged += _ => updateVisuals(); + EnableUserDim.ValueChanged += _ => UpdateVisuals(); + UserDimLevel.ValueChanged += _ => UpdateVisuals(); + ShowStoryboard.ValueChanged += _ => UpdateVisuals(); + StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); } protected override void LoadComplete() { base.LoadComplete(); - updateVisuals(); + UpdateVisuals(); } - private void updateVisuals() + /// + /// Whether the content of this container should currently be visible. + /// + protected virtual bool ShowDimContent => true; + + /// + /// Should be invoked when any dependent dim level or user setting is changed and bring the visual state up-to-date. + /// + protected virtual void UpdateVisuals() { - if (isStoryboard) - { - DimContainer.FadeTo(!showStoryboard.Value || userDimLevel.Value == 1 ? 0 : 1, background_fade_duration, Easing.OutQuint); - } - else - { - // The background needs to be hidden in the case of it being replaced by the storyboard - DimContainer.FadeTo(showStoryboard.Value && StoryboardReplacesBackground.Value ? 0 : 1, background_fade_duration, Easing.OutQuint); + ContentDisplayed = ShowDimContent; - Background?.BlurTo(blurTarget, background_fade_duration, Easing.OutQuint); - } - - DimContainer.FadeColour(EnableUserDim.Value ? OsuColour.Gray(1 - (float)userDimLevel.Value) : Color4.White, background_fade_duration, Easing.OutQuint); + dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); + dimContent.FadeColour(OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 092a23e787..e103798355 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -150,7 +150,7 @@ namespace osu.Game.Graphics.Cursor }, AdditiveLayer = new Sprite { - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = colour.Pink, Alpha = 0, Texture = textures.Get(@"Cursor/menu-cursor-additive"), diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index cfcda892fd..57f39bb8c7 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -30,22 +30,24 @@ namespace osu.Game.Graphics.Cursor private readonly OsuSpriteText text; private bool instantMovement = true; - public override string TooltipText + public override bool SetContent(object content) { - set + if (!(content is string contentString)) + return false; + + if (contentString == text.Text) return true; + + text.Text = contentString; + + if (IsPresent) { - if (value == text.Text) return; - - text.Text = value; - - if (IsPresent) - { - AutoSizeDuration = 250; - background.FlashColour(OsuColour.Gray(0.4f), 1000, Easing.OutQuint); - } - else - AutoSizeDuration = 0; + AutoSizeDuration = 250; + background.FlashColour(OsuColour.Gray(0.4f), 1000, Easing.OutQuint); } + else + AutoSizeDuration = 0; + + return true; } public OsuTooltip() diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 125c994c92..533f02af7b 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Sprites; +using osu.Game.Utils; namespace osu.Game.Graphics { @@ -71,7 +71,7 @@ namespace osu.Game.Graphics Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate); } - protected virtual string Format() => Date.Humanize(); + protected virtual string Format() => HumanizerUtils.Humanize(Date); private void updateTime() => Text = Format(); diff --git a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs new file mode 100644 index 0000000000..8714138322 --- /dev/null +++ b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; +using osu.Framework.Graphics.OpenGL.Vertices; +using osuTK; +using osuTK.Graphics; +using osuTK.Graphics.ES30; + +namespace osu.Game.Graphics.OpenGL.Vertices +{ + [StructLayout(LayoutKind.Sequential)] + public struct PositionAndColourVertex : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + + public bool Equals(PositionAndColourVertex other) + => Position.Equals(other.Position) + && Colour.Equals(other.Colour); + } +} diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 63ec24f84f..af66f57f14 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Beatmaps; using osuTK.Graphics; namespace osu.Game.Graphics @@ -37,6 +38,31 @@ namespace osu.Game.Graphics } } + public Color4 ForDifficultyRating(DifficultyRating difficulty) + { + switch (difficulty) + { + case DifficultyRating.Easy: + return Green; + + default: + case DifficultyRating.Normal: + return Blue; + + case DifficultyRating.Hard: + return Yellow; + + case DifficultyRating.Insane: + return Pink; + + case DifficultyRating.Expert: + return Purple; + + case DifficultyRating.ExpertPlus: + return Gray0; + } + } + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = FromHex(@"eeeeff"); public readonly Color4 PurpleLight = FromHex(@"aa88ff"); diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 5ad5e5569a..f532302de2 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -22,7 +22,7 @@ using SixLabors.ImageSharp; namespace osu.Game.Graphics { - public class ScreenshotManager : Container, IKeyBindingHandler, IHandleGlobalInput + public class ScreenshotManager : Container, IKeyBindingHandler, IHandleGlobalKeyboardInput { private readonly BindableBool cursorVisibility = new BindableBool(true); @@ -92,7 +92,8 @@ namespace osu.Game.Graphics using (var image = await host.TakeScreenshotAsync()) { - Interlocked.Decrement(ref screenShotTasks); + if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false) + cursorVisibility.Value = true; var fileName = getFileName(); if (fileName == null) return; @@ -125,14 +126,6 @@ namespace osu.Game.Graphics } }); - protected override void Update() - { - base.Update(); - - if (cursorVisibility.Value == false && Interlocked.CompareExchange(ref screenShotTasks, 0, 0) == 0) - cursorVisibility.Value = true; - } - private string getFileName() { var dt = DateTime.Now; diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs new file mode 100644 index 0000000000..24816deeb5 --- /dev/null +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.Sprites +{ + public class GlowingSpriteText : Container, IHasText + { + private readonly OsuSpriteText spriteText, blurredText; + + public string Text + { + get => spriteText.Text; + set => blurredText.Text = spriteText.Text = value; + } + + public FontUsage Font + { + get => spriteText.Font; + set => blurredText.Font = spriteText.Font = value.With(fixedWidth: true); + } + + public Vector2 TextSize + { + get => spriteText.Size; + set => blurredText.Size = spriteText.Size = value; + } + + public ColourInfo TextColour + { + get => spriteText.Colour; + set => spriteText.Colour = value; + } + + public ColourInfo GlowColour + { + get => blurredText.Colour; + set => blurredText.Colour = value; + } + + public GlowingSpriteText() + { + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new BufferedContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BlurSigma = new Vector2(4), + CacheDrawnFrameBuffer = true, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Size = new Vector2(3f), + Children = new[] + { + blurredText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + }, + }, + }, + spriteText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + }, + }; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs index 48bf0848ae..c4735b4a16 100644 --- a/osu.Game/Graphics/UserInterface/BackButton.cs +++ b/osu.Game/Graphics/UserInterface/BackButton.cs @@ -22,8 +22,8 @@ namespace osu.Game.Graphics.UserInterface Child = button = new TwoLayerButton { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, Text = @"back", Icon = OsuIcon.LeftCircle, Action = () => Action?.Invoke() diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs index 2a858ccbcf..f8d5955503 100644 --- a/osu.Game/Graphics/UserInterface/Bar.cs +++ b/osu.Game/Graphics/UserInterface/Bar.cs @@ -110,7 +110,7 @@ namespace osu.Game.Graphics.UserInterface [Flags] public enum BarDirection { - LeftToRight = 1 << 0, + LeftToRight = 1, RightToLeft = 1 << 1, TopToBottom = 1 << 2, BottomToTop = 1 << 3, diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index b50bf14bab..927ad13829 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -254,7 +254,7 @@ namespace osu.Game.Graphics.UserInterface colourContainer.Add(flash); flash.Colour = ButtonColour; - flash.Blending = BlendingMode.Additive; + flash.Blending = BlendingParameters.Additive; flash.Alpha = 0.3f; flash.FadeOutFromOne(click_duration); flash.Expire(); diff --git a/osu.Game/Graphics/UserInterface/DimmedLoadingLayer.cs b/osu.Game/Graphics/UserInterface/DimmedLoadingLayer.cs new file mode 100644 index 0000000000..b7d2222f33 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/DimmedLoadingLayer.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Extensions.Color4Extensions; + +namespace osu.Game.Graphics.UserInterface +{ + public class DimmedLoadingLayer : VisibilityContainer + { + private const float transition_duration = 250; + + private readonly LoadingAnimation loading; + + public DimmedLoadingLayer() + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + loading = new LoadingAnimation(), + }; + } + + protected override void PopIn() + { + this.FadeIn(transition_duration, Easing.OutQuint); + loading.Show(); + } + + protected override void PopOut() + { + this.FadeOut(transition_duration, Easing.OutQuint); + loading.Hide(); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index 757a9a349c..6d65b77cbf 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -76,7 +76,12 @@ namespace osu.Game.Graphics.UserInterface { Masking = true, RelativeSizeAxes = Axes.Both, - Child = path = new SmoothPath { RelativeSizeAxes = Axes.Both, PathRadius = 1 } + Child = path = new SmoothPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + PathRadius = 1 + } }); } @@ -88,7 +93,7 @@ namespace osu.Game.Graphics.UserInterface return base.Invalidate(invalidation, source, shallPropagate); } - private Cached pathCached = new Cached(); + private readonly Cached pathCached = new Cached(); protected override void Update() { diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 1a8fea4ff9..660bd7979f 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface { RelativeSizeAxes = Axes.Both, Colour = HoverColour, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0, }, } diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 7a27f825f6..c1810800a0 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -39,7 +39,7 @@ namespace osu.Game.Graphics.UserInterface hover = new Box { RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = Color4.White.Opacity(0.1f), Alpha = 0, Depth = -1 diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 47324ee646..6593531099 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -72,17 +72,11 @@ namespace osu.Game.Graphics.UserInterface Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load(AudioManager audio) { - base.LoadComplete(); - - Current.ValueChanged += enabled => - { - if (enabled.NewValue) - sampleChecked?.Play(); - else - sampleUnchecked?.Play(); - }; + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); } protected override bool OnHover(HoverEvent e) @@ -99,11 +93,13 @@ namespace osu.Game.Graphics.UserInterface base.OnHoverLost(e); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + protected override void OnUserChange(bool value) { - sampleChecked = audio.Samples.Get(@"UI/check-on"); - sampleUnchecked = audio.Samples.Get(@"UI/check-off"); + base.OnUserChange(value); + if (value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 11f41b1a48..c55d14456b 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -31,6 +31,11 @@ namespace osu.Game.Graphics.UserInterface protected virtual float StripWidth() => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; protected virtual float StripHeight() => 1; + /// + /// Whether entries should be automatically populated if is an type. + /// + protected virtual bool AddEnumEntriesAutomatically => true; + private static bool isEnumType => typeof(T).IsEnum; public OsuTabControl() @@ -45,7 +50,7 @@ namespace osu.Game.Graphics.UserInterface Colour = Color4.White.Opacity(0), }); - if (isEnumType) + if (isEnumType && AddEnumEntriesAutomatically) foreach (var val in (T[])Enum.GetValues(typeof(T))) AddItem(val); } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index 8134cfb42d..544acc7eb2 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface Direction = FillDirection.Horizontal, Children = new Drawable[] { - text = new OsuSpriteText { Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) }, + text = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) }, icon = new SpriteIcon { Size = new Vector2(14), @@ -81,10 +81,15 @@ namespace osu.Game.Graphics.UserInterface Colour = Color4.White, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - } + }, + new HoverClickSounds() }; - Current.ValueChanged += selected => { icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; }; + Current.ValueChanged += selected => + { + icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; + text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium); + }; } [BackgroundDependencyLoader] diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 156a556b5e..a0d3745180 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -24,7 +24,13 @@ namespace osu.Game.Graphics.UserInterface Height = 30; } - public class PageTabItem : TabItem + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.Yellow; + } + + public class PageTabItem : TabItem, IHasAccentColour { private const float transition_duration = 100; @@ -32,6 +38,18 @@ namespace osu.Game.Graphics.UserInterface protected readonly SpriteText Text; + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + accentColour = value; + box.Colour = accentColour; + } + } + public PageTabItem(T value) : base(value) { @@ -63,12 +81,6 @@ namespace osu.Game.Graphics.UserInterface Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - box.Colour = colours.Yellow; - } - protected override bool OnHover(HoverEvent e) { if (!Active.Value) diff --git a/osu.Game/Graphics/UserInterface/ScreenTitle.cs b/osu.Game/Graphics/UserInterface/ScreenTitle.cs index 7b39238e5e..10fc312d8b 100644 --- a/osu.Game/Graphics/UserInterface/ScreenTitle.cs +++ b/osu.Game/Graphics/UserInterface/ScreenTitle.cs @@ -15,7 +15,7 @@ namespace osu.Game.Graphics.UserInterface { public const float ICON_WIDTH = ICON_SIZE + icon_spacing; - protected const float ICON_SIZE = 25; + public const float ICON_SIZE = 25; private SpriteIcon iconSprite; private readonly OsuSpriteText titleText, pageText; diff --git a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs new file mode 100644 index 0000000000..f590e7e357 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// A custom icon class for use with based off a texture resource. + /// + public class ScreenTitleTextureIcon : CompositeDrawable + { + private const float circle_allowance = 0.8f; + + private readonly string textureName; + + public ScreenTitleTextureIcon(string textureName) + { + this.textureName = textureName; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures, OsuColour colours) + { + Size = new Vector2(ScreenTitle.ICON_SIZE / circle_allowance); + + InternalChildren = new Drawable[] + { + new CircularContainer + { + Masking = true, + BorderColour = colours.Violet, + BorderThickness = 3, + MaskingSmoothness = 1, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(textureName), + Size = new Vector2(circle_allowance), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Violet, + Alpha = 0, + AlwaysPresent = true, + }, + } + }, + }; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index 7023711aaa..c3efe2ed45 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Events; using osuTK; using osuTK.Input; @@ -33,6 +34,17 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = "type to search"; } + public override bool OnPressed(PlatformAction action) + { + // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox + // as we do not allow arrow key navigation in the first place (ie. the care should always be at the end of text) + // Avoid handling it here to allow other components to potentially consume the shortcut. + if (action.ActionType == PlatformActionType.CharNext && action.ActionMethod == PlatformActionMethod.Delete) + return false; + + return base.OnPressed(action); + } + protected override bool OnKeyDown(KeyDownEvent e) { if (!e.ControlPressed && !e.ShiftPressed) diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index c74d00cd1e..aa96796cf1 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -15,6 +15,7 @@ using System; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Screens.Select; namespace osu.Game.Graphics.UserInterface { @@ -28,7 +29,9 @@ namespace osu.Game.Graphics.UserInterface private const int transform_time = 600; private const int pulse_length = 250; - private const float shear = 0.1f; + private const float shear_width = 5f; + + private static readonly Vector2 shear = new Vector2(shear_width / Footer.HEIGHT, 0); public static readonly Vector2 SIZE_EXTENDED = new Vector2(140, 50); public static readonly Vector2 SIZE_RETRACTED = new Vector2(100, 50); @@ -56,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; - X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear * 0.5f : 0; + X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; Remove(c1); Remove(c2); @@ -70,6 +73,7 @@ namespace osu.Game.Graphics.UserInterface public TwoLayerButton() { Size = SIZE_RETRACTED; + Shear = shear; Children = new Drawable[] { @@ -82,7 +86,6 @@ namespace osu.Game.Graphics.UserInterface new Container { RelativeSizeAxes = Axes.Both, - Shear = new Vector2(shear, 0), Masking = true, MaskingSmoothness = 2, EdgeEffect = new EdgeEffectParameters @@ -105,6 +108,7 @@ namespace osu.Game.Graphics.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Shear = -shear, }, } }, @@ -119,7 +123,6 @@ namespace osu.Game.Graphics.UserInterface new Container { RelativeSizeAxes = Axes.Both, - Shear = new Vector2(shear, 0), Masking = true, MaskingSmoothness = 2, EdgeEffect = new EdgeEffectParameters @@ -144,6 +147,7 @@ namespace osu.Game.Graphics.UserInterface { Origin = Anchor.Centre, Anchor = Anchor.Centre, + Shear = -shear, } } }, @@ -188,7 +192,6 @@ namespace osu.Game.Graphics.UserInterface var flash = new Box { RelativeSizeAxes = Axes.Both, - Shear = new Vector2(shear, 0), Colour = Color4.White.Opacity(0.5f), }; Add(flash); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 14d356f889..bf758e21d9 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -10,7 +10,7 @@ using osu.Framework.Input.Bindings; namespace osu.Game.Input.Bindings { - public class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalInput + public class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput { private readonly Drawable handler; @@ -20,7 +20,7 @@ namespace osu.Game.Input.Bindings handler = game; } - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings); + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings); public IEnumerable GlobalKeyBindings => new[] { @@ -32,14 +32,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), - new KeyBinding(InputKey.Up, GlobalAction.IncreaseVolume), - new KeyBinding(InputKey.MouseWheelUp, GlobalAction.IncreaseVolume), - new KeyBinding(InputKey.Down, GlobalAction.DecreaseVolume), - new KeyBinding(InputKey.MouseWheelDown, GlobalAction.DecreaseVolume), - new KeyBinding(InputKey.F4, GlobalAction.ToggleMute), new KeyBinding(InputKey.Escape, GlobalAction.Back), - new KeyBinding(InputKey.MouseButton1, GlobalAction.Back), + new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select), @@ -55,6 +50,22 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), }; + public IEnumerable AudioControlKeyBindings => new[] + { + new KeyBinding(InputKey.Up, GlobalAction.IncreaseVolume), + new KeyBinding(InputKey.MouseWheelUp, GlobalAction.IncreaseVolume), + new KeyBinding(InputKey.Down, GlobalAction.DecreaseVolume), + new KeyBinding(InputKey.MouseWheelDown, GlobalAction.DecreaseVolume), + new KeyBinding(InputKey.F4, GlobalAction.ToggleMute), + + new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev), + new KeyBinding(InputKey.F1, GlobalAction.MusicPrev), + new KeyBinding(InputKey.TrackNext, GlobalAction.MusicNext), + new KeyBinding(InputKey.F5, GlobalAction.MusicNext), + new KeyBinding(InputKey.PlayPause, GlobalAction.MusicPlay), + new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) + }; + protected override IEnumerable KeyBindingInputQueue => handler == null ? base.KeyBindingInputQueue : base.KeyBindingInputQueue.Prepend(handler); } @@ -115,5 +126,15 @@ namespace osu.Game.Input.Bindings [Description("Quick exit (Hold)")] QuickExit, + + // Game-wide beatmap msi ccotolle keybindings + [Description("Next track")] + MusicNext, + + [Description("Previous track")] + MusicPrev, + + [Description("Play / pause")] + MusicPlay, } } diff --git a/osu.Game/Input/IdleTracker.cs b/osu.Game/Input/IdleTracker.cs index cbc446a126..39ccf9fe1c 100644 --- a/osu.Game/Input/IdleTracker.cs +++ b/osu.Game/Input/IdleTracker.cs @@ -12,7 +12,7 @@ namespace osu.Game.Input /// /// Track whether the end-user is in an idle state, based on their last interaction with the game. /// - public class IdleTracker : Component, IKeyBindingHandler, IHandleGlobalInput + public class IdleTracker : Component, IKeyBindingHandler, IHandleGlobalKeyboardInput { private readonly double timeToIdle; diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs index 45d751f00e..fb1385793f 100644 --- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs @@ -7,27 +7,27 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserBeatmapsRequest : APIRequest> + public class GetUserBeatmapsRequest : PaginatedAPIRequest> { private readonly long userId; - private readonly int offset; + private readonly BeatmapSetType type; - public GetUserBeatmapsRequest(long userId, BeatmapSetType type, int offset = 0) + public GetUserBeatmapsRequest(long userId, BeatmapSetType type, int page = 0, int itemsPerPage = 6) + : base(page, itemsPerPage) { this.userId = userId; - this.offset = offset; this.type = type; } - // ReSharper disable once ImpureMethodCallOnReadonlyValueField - protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}?offset={offset}"; + protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}"; } public enum BeatmapSetType { Favourite, RankedAndApproved, + Loved, Unranked, Graveyard } diff --git a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs index 40e52bdaf6..9f094e51c4 100644 --- a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs @@ -6,17 +6,16 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserMostPlayedBeatmapsRequest : APIRequest> + public class GetUserMostPlayedBeatmapsRequest : PaginatedAPIRequest> { private readonly long userId; - private readonly int offset; - public GetUserMostPlayedBeatmapsRequest(long userId, int offset = 0) + public GetUserMostPlayedBeatmapsRequest(long userId, int page = 0, int itemsPerPage = 5) + : base(page, itemsPerPage) { this.userId = userId; - this.offset = offset; } - protected override string Target => $@"users/{userId}/beatmapsets/most_played?offset={offset}"; + protected override string Target => $@"users/{userId}/beatmapsets/most_played"; } } diff --git a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs index 9f80180e70..4908e5ecc2 100644 --- a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs @@ -6,18 +6,17 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserRecentActivitiesRequest : APIRequest> + public class GetUserRecentActivitiesRequest : PaginatedAPIRequest> { private readonly long userId; - private readonly int offset; - public GetUserRecentActivitiesRequest(long userId, int offset = 0) + public GetUserRecentActivitiesRequest(long userId, int page = 0, int itemsPerPage = 5) + : base(page, itemsPerPage) { this.userId = userId; - this.offset = offset; } - protected override string Target => $"users/{userId}/recent_activity?offset={offset}"; + protected override string Target => $"users/{userId}/recent_activity"; } public enum RecentActivityType diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index 37ed0574f1..31b7e95b39 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -2,18 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Users; +using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { public class GetUserRequest : APIRequest { private readonly long? userId; + private readonly RulesetInfo ruleset; - public GetUserRequest(long? userId = null) + public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) { this.userId = userId; + this.ruleset = ruleset; } - protected override string Target => userId.HasValue ? $@"users/{userId}" : @"me"; + protected override string Target => userId.HasValue ? $@"users/{userId}/{ruleset?.ShortName}" : $@"me/{ruleset?.ShortName}"; } } diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index 48a43bbbad..7b4d66e7b2 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -2,25 +2,37 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.IO.Network; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public class GetUserScoresRequest : APIRequest> + public class GetUserScoresRequest : PaginatedAPIRequest> { private readonly long userId; private readonly ScoreType type; - private readonly int offset; + private readonly RulesetInfo ruleset; - public GetUserScoresRequest(long userId, ScoreType type, int offset = 0) + public GetUserScoresRequest(long userId, ScoreType type, int page = 0, int itemsPerPage = 5, RulesetInfo ruleset = null) + : base(page, itemsPerPage) { this.userId = userId; this.type = type; - this.offset = offset; + this.ruleset = ruleset; } - // ReSharper disable once ImpureMethodCallOnReadonlyValueField - protected override string Target => $@"users/{userId}/scores/{type.ToString().ToLowerInvariant()}?offset={offset}"; + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + if (ruleset != null) + req.AddParameter("mode", ruleset.ShortName); + + return req; + } + + protected override string Target => $@"users/{userId}/scores/{type.ToString().ToLowerInvariant()}"; } public enum ScoreType diff --git a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs new file mode 100644 index 0000000000..52e12f04ee --- /dev/null +++ b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public abstract class PaginatedAPIRequest : APIRequest + { + private readonly int page; + private readonly int itemsPerPage; + + protected PaginatedAPIRequest(int page, int itemsPerPage) + { + this.page = page; + this.itemsPerPage = itemsPerPage; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.AddParameter("offset", (page * itemsPerPage).ToString(CultureInfo.InvariantCulture)); + req.AddParameter("limit", itemsPerPage.ToString(CultureInfo.InvariantCulture)); + + return req; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 9ca2cb0b81..1526cccf31 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -30,6 +30,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"preview_url")] private string preview { get; set; } + [JsonProperty(@"has_favourited")] + private bool hasFavourited { get; set; } + [JsonProperty(@"play_count")] private int playCount { get; set; } @@ -97,6 +100,7 @@ namespace osu.Game.Online.API.Requests.Responses Ranked = ranked, LastUpdated = lastUpdated, Availability = availability, + HasFavourited = hasFavourited, Genre = genre, Language = language }, diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index d9d05ff285..c8c36789c4 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -27,24 +27,25 @@ namespace osu.Game.Online.API.Requests } // ReSharper disable once ImpureMethodCallOnReadonlyValueField - protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={(int)searchCategory}&sort={sortCriteria.ToString().ToLowerInvariant()}_{directionString}"; + protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={searchCategory.ToString().ToLowerInvariant()}&sort={sortCriteria.ToString().ToLowerInvariant()}_{directionString}"; } public enum BeatmapSearchCategory { - Any = 7, + Any, - [Description("Ranked & Approved")] - RankedApproved = 0, - Qualified = 3, - Loved = 8, - Favourites = 2, + [Description("Has Leaderboard")] + Leaderboard, + Ranked, + Qualified, + Loved, + Favourites, [Description("Pending & WIP")] - PendingWIP = 4, - Graveyard = 5, + Pending, + Graveyard, [Description("My Maps")] - MyMaps = 6, + Mine, } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 3af11ff20f..4f6066cab1 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -213,8 +213,27 @@ namespace osu.Game.Online.Chat PostMessage(content, true); break; + case "join": + if (string.IsNullOrWhiteSpace(content)) + { + target.AddNewMessages(new ErrorMessage("Usage: /join [channel]")); + break; + } + + var channel = availableChannels.Where(c => c.Name == content || c.Name == $"#{content}").FirstOrDefault(); + + if (channel == null) + { + target.AddNewMessages(new ErrorMessage($"Channel '{content}' not found.")); + break; + } + + JoinChannel(channel); + CurrentChannel.Value = channel; + break; + case "help": - target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]")); + target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]")); break; default: diff --git a/osu.Game/Online/Chat/ErrorMessage.cs b/osu.Game/Online/Chat/ErrorMessage.cs index a8ff0e9a98..87a65fb3f1 100644 --- a/osu.Game/Online/Chat/ErrorMessage.cs +++ b/osu.Game/Online/Chat/ErrorMessage.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Chat public ErrorMessage(string message) : base(message) { - Sender.Colour = @"ff0000"; + // todo: this should likely be styled differently in the future. } } } diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 4aaffdd161..db26945ef3 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -122,6 +122,7 @@ namespace osu.Game.Online.Chat return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); case "u": + case "users": return new LinkDetails(LinkAction.OpenUserProfile, args[3]); } } diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 62d6efcb6f..7bfdc7ff69 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -48,22 +48,24 @@ namespace osu.Game.Online attachDownload(manager.GetExistingDownload(modelInfo.NewValue)); }, true); - manager.DownloadBegan += download => - { - if (download.Model.Equals(Model.Value)) - attachDownload(download); - }; - - manager.DownloadFailed += download => - { - if (download.Model.Equals(Model.Value)) - attachDownload(null); - }; - + manager.DownloadBegan += downloadBegan; + manager.DownloadFailed += downloadFailed; manager.ItemAdded += itemAdded; manager.ItemRemoved += itemRemoved; } + private void downloadBegan(ArchiveDownloadRequest request) + { + if (request.Model.Equals(Model.Value)) + attachDownload(request); + } + + private void downloadFailed(ArchiveDownloadRequest request) + { + if (request.Model.Equals(Model.Value)) + attachDownload(null); + } + private ArchiveDownloadRequest attachedRequest; private void attachDownload(ArchiveDownloadRequest request) @@ -126,8 +128,10 @@ namespace osu.Game.Online if (manager != null) { - manager.DownloadBegan -= attachDownload; + manager.DownloadBegan -= downloadBegan; + manager.DownloadFailed -= downloadFailed; manager.ItemAdded -= itemAdded; + manager.ItemRemoved -= itemRemoved; } State.UnbindAll(); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 35f7ba1c1b..98f15599fc 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -51,7 +51,6 @@ namespace osu.Game.Online.Leaderboards loading.Hide(); - // schedule because we may not be loaded yet (LoadComponentAsync complains). showScoresDelegate?.Cancel(); showScoresCancellationSource?.Cancel(); @@ -61,28 +60,22 @@ namespace osu.Game.Online.Leaderboards // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; - scrollFlow = CreateScoreFlow(); - scrollFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); + var sf = CreateScoreFlow(); + sf.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); - if (!IsLoaded) - showScoresDelegate = Schedule(showScores); - else - showScores(); - - void showScores() => LoadComponentAsync(scrollFlow, _ => + // schedule because we may not be loaded yet (LoadComponentAsync complains). + showScoresDelegate = Schedule(() => LoadComponentAsync(sf, _ => { - scrollContainer.Add(scrollFlow); + scrollContainer.Add(scrollFlow = sf); int i = 0; foreach (var s in scrollFlow.Children) - { using (s.BeginDelayedSequence(i++ * 50, true)) s.Show(); - } scrollContainer.ScrollTo(0f, false); - }, (showScoresCancellationSource = new CancellationTokenSource()).Token); + }, (showScoresCancellationSource = new CancellationTokenSource()).Token)); } } @@ -201,13 +194,17 @@ namespace osu.Game.Online.Leaderboards private APIRequest getScoresRequest; + protected abstract bool IsOnlineScope { get; } + public void APIStateChanged(IAPIProvider api, APIState state) { switch (state) { case APIState.Online: case APIState.Offline: - UpdateScores(); + if (IsOnlineScope) + UpdateScores(); + break; } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 9840b59805..008f8208eb 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -187,7 +187,13 @@ namespace osu.Game.Online.Leaderboards Spacing = new Vector2(5f, 0f), Children = new Drawable[] { - scoreLabel = new GlowingSpriteText(score.TotalScore.ToString(@"N0"), OsuFont.Numeric.With(size: 23), Color4.White, OsuColour.FromHex(@"83ccfa")), + scoreLabel = new GlowingSpriteText + { + TextColour = Color4.White, + GlowColour = OsuColour.FromHex(@"83ccfa"), + Text = score.TotalScore.ToString(@"N0"), + Font = OsuFont.Numeric.With(size: 23), + }, RankContainer = new Container { Size = new Vector2(40f, 20f), @@ -275,49 +281,6 @@ namespace osu.Game.Online.Leaderboards base.OnHoverLost(e); } - private class GlowingSpriteText : Container - { - public GlowingSpriteText(string text, FontUsage font, Color4 textColour, Color4 glowColour) - { - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - new BufferedContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BlurSigma = new Vector2(4), - CacheDrawnFrameBuffer = true, - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Size = new Vector2(3f), - Children = new[] - { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = font.With(fixedWidth: true), - Text = text, - Colour = glowColour, - Shadow = false, - }, - }, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = font.With(fixedWidth: true), - Text = text, - Colour = textColour, - Shadow = false, - }, - }; - } - } - private class ScoreComponentLabel : Container, IHasTooltip { private const float icon_size = 20; @@ -367,10 +330,14 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new GlowingSpriteText(statistic.Value, OsuFont.GetFont(size: 17, weight: FontWeight.Bold), Color4.White, OsuColour.FromHex(@"83ccfa")) + new GlowingSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + TextColour = Color4.White, + GlowColour = OsuColour.FromHex(@"83ccfa"), + Text = statistic.Value, + Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold), }, }, }; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 361ff62155..0e804ecbaf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; @@ -86,7 +87,8 @@ namespace osu.Game private BackButton backButton; private MainMenu menuScreen; - private Intro introScreen; + + private IntroScreen introScreen; private Bindable configRuleset; @@ -153,7 +155,7 @@ namespace osu.Game { this.frameworkConfig = frameworkConfig; - if (!Host.IsPrimaryInstance) + if (!Host.IsPrimaryInstance && !DebugUtils.IsDebugBuild) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); Environment.Exit(0); @@ -248,7 +250,7 @@ namespace osu.Game } // Use first beatmap available for current ruleset, else switch ruleset. - var first = databasedSet.Beatmaps.Find(b => b.Ruleset == Ruleset.Value) ?? databasedSet.Beatmaps.First(); + var first = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First(); Ruleset.Value = first.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); @@ -263,7 +265,16 @@ namespace osu.Game { // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. - var databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID); + var databasedScoreInfo = score.OnlineScoreID != null + ? ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID) + : ScoreManager.Query(s => s.Hash == score.Hash); + + if (databasedScoreInfo == null) + { + Logger.Log("The requested score could not be found locally.", LoggingTarget.Information); + return; + } + var databasedScore = ScoreManager.GetScore(databasedScoreInfo); if (databasedScore.Replay == null) @@ -288,7 +299,7 @@ namespace osu.Game }, $"watch {databasedScoreInfo}", bypassScreenAllowChecks: true); } - #region Beatmap jukebox progression + #region Beatmap progression private void beatmapChanged(ValueChangedEvent beatmap) { @@ -296,7 +307,9 @@ namespace osu.Game if (nextBeatmap?.Track != null) nextBeatmap.Track.Completed += currentTrackCompleted; - beatmap.OldValue?.Dispose(); + using (var oldBeatmap = beatmap.OldValue) + if (oldBeatmap?.Track != null) + oldBeatmap.Track.Completed -= currentTrackCompleted; nextBeatmap?.LoadBeatmapAsync(); } @@ -456,6 +469,8 @@ namespace osu.Game loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add); loadComponentSingleFile(new OnScreenDisplay(), Add, true); + loadComponentSingleFile(musicController = new MusicController(), Add, true); + loadComponentSingleFile(notifications = new NotificationOverlay { GetToolbarHeight = () => ToolbarOffset, @@ -482,7 +497,7 @@ namespace osu.Game Origin = Anchor.TopRight, }, rightFloatingOverlayContent.Add, true); - loadComponentSingleFile(musicController = new MusicController + loadComponentSingleFile(new NowPlayingOverlay { GetToolbarHeight = () => ToolbarOffset, Anchor = Anchor.TopRight, @@ -588,7 +603,7 @@ namespace osu.Game { int recentLogCount = 0; - const double debounce = 5000; + const double debounce = 60000; Logger.NewEntry += entry => { @@ -760,7 +775,7 @@ namespace osu.Game if (introScreen == null) return true; - if (!introScreen.DidLoadMenu || !(screenStack.CurrentScreen is Intro)) + if (!introScreen.DidLoadMenu || !(screenStack.CurrentScreen is IntroScreen)) { Scheduler.Add(introScreen.MakeCurrent); return true; @@ -795,7 +810,7 @@ namespace osu.Game { switch (newScreen) { - case Intro intro: + case IntroScreen intro: introScreen = intro; break; diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index baf702eebc..28947b6f22 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -91,7 +91,8 @@ namespace osu.Game.Overlays.BeatmapSet { difficulties = new DifficultiesContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2) }, OnLostHover = () => { @@ -234,7 +235,7 @@ namespace osu.Game.Overlays.BeatmapSet Colour = Color4.Black.Opacity(0.5f), }, }, - icon = new DifficultyIcon(beatmap) + icon = new DifficultyIcon(beatmap, shouldShowTooltip: false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index 7207739646..11f56bc163 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osuTK; @@ -15,7 +16,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { public class FavouriteButton : HeaderButton { - public readonly Bindable Favourited = new Bindable(); + public readonly Bindable BeatmapSet = new Bindable(); + + private readonly Bindable favourited = new Bindable(); [BackgroundDependencyLoader] private void load() @@ -54,7 +57,15 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons }, }); - Favourited.ValueChanged += favourited => + BeatmapSet.BindValueChanged(setInfo => + { + if (setInfo.NewValue?.OnlineInfo?.HasFavourited == null) + return; + + favourited.Value = setInfo.NewValue.OnlineInfo.HasFavourited; + }); + + favourited.ValueChanged += favourited => { if (favourited.NewValue) { @@ -67,8 +78,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons icon.Icon = FontAwesome.Regular.Heart; } }; - - Action = () => Favourited.Value = !Favourited.Value; } protected override void UpdateAfterChildren() diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index b50eac2c1a..260a989628 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -161,7 +161,10 @@ namespace osu.Game.Overlays.BeatmapSet Margin = new MarginPadding { Top = 10 }, Children = new Drawable[] { - favouriteButton = new FavouriteButton(), + favouriteButton = new FavouriteButton + { + BeatmapSet = { BindTarget = BeatmapSet } + }, downloadButtonsContainer = new FillFlowContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs new file mode 100644 index 0000000000..dcd58db427 --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.UserInterface; +using osu.Game.Screens.Select.Leaderboards; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Framework.Allocation; +using osuTK.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Input.Events; + +namespace osu.Game.Overlays.BeatmapSet +{ + public class LeaderboardScopeSelector : PageTabControl + { + protected override bool AddEnumEntriesAutomatically => false; + + protected override Dropdown CreateDropdown() => null; + + protected override TabItem CreateTabItem(BeatmapLeaderboardScope value) => new ScopeSelectorTabItem(value); + + public LeaderboardScopeSelector() + { + RelativeSizeAxes = Axes.X; + + AddItem(BeatmapLeaderboardScope.Global); + AddItem(BeatmapLeaderboardScope.Country); + AddItem(BeatmapLeaderboardScope.Friend); + + AddInternal(new GradientLine + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.Blue; + } + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20, 0), + }; + + private class ScopeSelectorTabItem : PageTabItem + { + public ScopeSelectorTabItem(BeatmapLeaderboardScope value) + : base(value) + { + Text.Font = OsuFont.GetFont(size: 16); + } + + protected override bool OnHover(HoverEvent e) + { + Text.FadeColour(AccentColour); + + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + Text.FadeColour(Color4.White); + } + } + + private class GradientLine : GridContainer + { + public GradientLine() + { + RelativeSizeAxes = Axes.X; + Size = new Vector2(0.8f, 1.5f); + + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(mode: GridSizeMode.Relative, size: 0.4f), + new Dimension(), + }; + + Content = new[] + { + new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Transparent, Color4.Gray), + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Gray, Color4.Transparent), + }, + } + }; + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 22d7ea9c97..4bbcd8d631 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -19,7 +19,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores public class ScoresContainer : CompositeDrawable { private const int spacing = 15; - private const int fade_duration = 200; private readonly Box background; private readonly ScoreTable scoreTable; @@ -53,8 +52,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Schedule(() => { - loading = false; - topScoresContainer.Clear(); if (value?.Scores.Any() != true) @@ -128,11 +125,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores background.Colour = colours.Gray2; } - private bool loading - { - set => loadingAnimation.FadeTo(value ? 1 : 0, fade_duration); - } - private void getScores(BeatmapInfo beatmap) { getScoresRequest?.Cancel(); @@ -140,16 +132,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Scores = null; - if (beatmap?.OnlineBeatmapID.HasValue != true) - { - loading = false; + if (beatmap?.OnlineBeatmapID.HasValue != true || beatmap.Status <= BeatmapSetOnlineStatus.Pending) return; - } + loadingAnimation.Show(); getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset); - getScoresRequest.Success += scores => Scores = scores; + getScoresRequest.Success += scores => + { + loadingAnimation.Hide(); + Scores = scores; + }; api.Queue(getScoresRequest); - loading = true; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index a15d3c5fd1..38a909411a 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -14,6 +13,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; using osu.Game.Users.Drawables; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - avatar = new UpdateableAvatar(hideImmediately: true) + avatar = new UpdateableAvatar { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold) }, - flag = new UpdateableFlag(hideImmediately: true) + flag = new UpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { avatar.User = value.User; flag.Country = value.User.Country; - date.Text = $@"achieved {value.Date.Humanize()}"; + date.Text = $@"achieved {HumanizerUtils.Humanize(value.Date)}"; usernameText.Clear(); usernameText.AddUserLink(value.User); diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index fca62fbb44..b2e9be24b3 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -7,13 +7,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; -using osuTK; namespace osu.Game.Overlays.Changelog { @@ -123,48 +121,7 @@ namespace osu.Game.Overlays.Changelog AccentColour = colours.Violet; } - protected override Drawable CreateIcon() => new ChangelogIcon(); - - internal class ChangelogIcon : CompositeDrawable - { - private const float circle_allowance = 0.8f; - - [BackgroundDependencyLoader] - private void load(TextureStore textures, OsuColour colours) - { - Size = new Vector2(ICON_SIZE / circle_allowance); - - InternalChildren = new Drawable[] - { - new CircularContainer - { - Masking = true, - BorderColour = colours.Violet, - BorderThickness = 3, - MaskingSmoothness = 1, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get(@"Icons/changelog"), - Size = new Vector2(circle_allowance), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Violet, - Alpha = 0, - AlwaysPresent = true, - }, - } - }, - }; - } - } + protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog"); } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs index 44552b214f..9c3504f477 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSingleBuild.cs @@ -92,24 +92,16 @@ namespace osu.Game.Overlays.Changelog }); } - NavigationIconButton left, right; - - fill.AddRange(new[] + fill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous) { - left = new NavigationIconButton(Build.Versions?.Previous) - { - Icon = FontAwesome.Solid.ChevronLeft, - SelectBuild = b => SelectBuild(b) - }, - right = new NavigationIconButton(Build.Versions?.Next) - { - Icon = FontAwesome.Solid.ChevronRight, - SelectBuild = b => SelectBuild(b) - }, + Icon = FontAwesome.Solid.ChevronLeft, + SelectBuild = b => SelectBuild(b) + }); + fill.Insert(1, new NavigationIconButton(Build.Versions?.Next) + { + Icon = FontAwesome.Solid.ChevronRight, + SelectBuild = b => SelectBuild(b) }); - - fill.SetLayoutPosition(left, -1); - fill.SetLayoutPosition(right, 1); return fill; } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index e29216dffd..2576b38ec8 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -85,6 +85,7 @@ namespace osu.Game.Overlays.Chat Drawable effectedUsername = username = new OsuSpriteText { + Shadow = false, Colour = hasBackground ? customUsernameColour : username_colours[message.Sender.Id % username_colours.Length], Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true) }; @@ -133,6 +134,7 @@ namespace osu.Game.Overlays.Chat { timestamp = new OsuSpriteText { + Shadow = false, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true) @@ -155,6 +157,8 @@ namespace osu.Game.Overlays.Chat { contentFlow = new LinkFlowContainer(t => { + t.Shadow = false; + if (Message.IsAction) { t.Font = OsuFont.GetFont(italics: true); diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 8d56e250fc..f831266b1b 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -30,27 +30,24 @@ namespace osu.Game.Overlays.Chat [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] + Child = new OsuContextMenuContainer { - scroll = new OsuScrollContainer + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = scroll = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, // Some chat lines have effects that slightly protrude to the bottom, // which we do not want to mask away, hence the padding. Padding = new MarginPadding { Bottom = 5 }, - Child = new OsuContextMenuContainer + Child = ChatLineFlow = new ChatLineContainer { + Padding = new MarginPadding { Left = 20, Right = 20 }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = ChatLineFlow = new ChatLineContainer - { - Padding = new MarginPadding { Left = 20, Right = 20 }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - } - }, - } + Direction = FillDirection.Vertical, + } + }, }; newMessagesArrived(Channel.Messages); diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index 7386bffb1a..d5d9a6c2ce 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -13,6 +13,8 @@ namespace osu.Game.Overlays.Chat.Tabs public override bool IsSwitchable => false; + protected override bool IsBoldWhenActive => false; + public ChannelSelectorTabItem() : base(new ChannelSelectorTabChannel()) { @@ -22,7 +24,7 @@ namespace osu.Game.Overlays.Chat.Tabs Icon.Alpha = 0; Text.Font = Text.Font.With(size: 45); - TextBold.Font = Text.Font.With(size: 45); + Text.Truncate = false; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs index 7f820e4ff7..266e68f17e 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Overlays.Chat.Tabs { @@ -28,7 +29,6 @@ namespace osu.Game.Overlays.Chat.Tabs public override bool IsRemovable => !Pinned; protected readonly SpriteText Text; - protected readonly SpriteText TextBold; protected readonly ClickableContainer CloseButton; private readonly Box box; private readonly Box highlightBox; @@ -87,20 +87,17 @@ namespace osu.Game.Overlays.Chat.Tabs }, Text = new OsuSpriteText { - Margin = new MarginPadding(5), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Text = value.ToString(), - Font = OsuFont.GetFont(size: 18) - }, - TextBold = new OsuSpriteText - { - Alpha = 0, - Margin = new MarginPadding(5), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Text = value.ToString(), - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: 18), + Padding = new MarginPadding(5) + { + Left = LeftTextPadding, + Right = RightTextPadding, + }, + RelativeSizeAxes = Axes.X, + Truncate = true, }, CloseButton = new TabCloseButton { @@ -118,10 +115,16 @@ namespace osu.Game.Overlays.Chat.Tabs }; } + protected virtual float LeftTextPadding => 5; + + protected virtual float RightTextPadding => IsRemovable ? 40 : 5; + protected virtual IconUsage DisplayIcon => FontAwesome.Solid.Hashtag; protected virtual bool ShowCloseOnHover => true; + protected virtual bool IsBoldWhenActive => true; + protected override bool OnHover(HoverEvent e) { if (IsRemovable && ShowCloseOnHover) @@ -138,6 +141,19 @@ namespace osu.Game.Overlays.Chat.Tabs updateState(); } + protected override bool OnMouseUp(MouseUpEvent e) + { + switch (e.Button) + { + case MouseButton.Middle: + CloseButton.Click(); + return true; + + default: + return false; + } + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -189,8 +205,7 @@ namespace osu.Game.Overlays.Chat.Tabs box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint); highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); - Text.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); - TextBold.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + if (IsBoldWhenActive) Text.Font = Text.Font.With(weight: FontWeight.Bold); } protected virtual void FadeInactive() @@ -202,8 +217,7 @@ namespace osu.Game.Overlays.Chat.Tabs box.FadeColour(BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint); highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); - Text.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); - TextBold.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + Text.Font = Text.Font.With(weight: FontWeight.Medium); } protected override void OnActivated() => updateState(); diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 9e87bae864..1413b8fe78 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -62,11 +62,10 @@ namespace osu.Game.Overlays.Chat.Tabs }); avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); - - Text.X = ChatOverlay.TAB_AREA_HEIGHT; - TextBold.X = ChatOverlay.TAB_AREA_HEIGHT; } + protected override float LeftTextPadding => base.LeftTextPadding + ChatOverlay.TAB_AREA_HEIGHT; + protected override bool ShowCloseOnHover => false; protected override void FadeActive() diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index e223856b27..53a05656b1 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -256,6 +256,9 @@ namespace osu.Game.Overlays loadedChannels.Add(loaded); LoadComponentAsync(loaded, l => { + if (currentChannel.Value != e.NewValue) + return; + loading.Hide(); currentChannelContainer.Clear(false); @@ -381,7 +384,18 @@ namespace osu.Game.Overlays foreach (Channel channel in channels) { ChannelTabControl.RemoveChannel(channel); - loadedChannels.Remove(loadedChannels.Find(c => c.Channel == channel)); + + var loaded = loadedChannels.Find(c => c.Channel == channel); + + if (loaded != null) + { + loadedChannels.Remove(loaded); + + // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared + // to ensure that the previous channel doesn't get updated after it's disposed + currentChannelContainer.Remove(loaded); + loaded.Dispose(); + } } } diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs index 243e79eb9b..7bf94c1483 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Direct AutoSizeAxes = Axes.X, Height = 20, Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, - Children = GetDifficultyIcons(), + Children = GetDifficultyIcons(colours), }, }, }, diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 5757e1445b..158ff648dd 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Direct AutoSizeAxes = Axes.X, Height = 20, Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, - Children = GetDifficultyIcons(), + Children = GetDifficultyIcons(colours), }, }, }, diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 8199d80528..641423f21f 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Direct public readonly BeatmapSetInfo SetInfo; private const double hover_transition_time = 400; + private const int maximum_difficulty_icons = 15; private Container content; @@ -138,12 +139,18 @@ namespace osu.Game.Overlays.Direct }; } - protected List GetDifficultyIcons() + protected List GetDifficultyIcons(OsuColour colours) { var icons = new List(); - foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty)) - icons.Add(new DifficultyIcon(b)); + if (SetInfo.Beatmaps.Count > maximum_difficulty_icons) + { + foreach (var ruleset in SetInfo.Beatmaps.Select(b => b.Ruleset).Distinct()) + icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is DirectListPanel ? Color4.White : colours.Gray5)); + } + else + foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty)) + icons.Add(new DifficultyIcon(b)); return icons; } diff --git a/osu.Game/Overlays/Direct/DirectRulesetSelector.cs b/osu.Game/Overlays/Direct/DirectRulesetSelector.cs index fdab9f1b90..106aaa616b 100644 --- a/osu.Game/Overlays/Direct/DirectRulesetSelector.cs +++ b/osu.Game/Overlays/Direct/DirectRulesetSelector.cs @@ -26,8 +26,13 @@ namespace osu.Game.Overlays.Direct TabContainer.Masking = false; TabContainer.Spacing = new Vector2(10, 0); AutoSizeAxes = Axes.Both; + } - Current.DisabledChanged += value => SelectedTab.FadeColour(value ? Color4.DarkGray : Color4.White, 200, Easing.OutQuint); + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindDisabledChanged(value => SelectedTab.FadeColour(value ? Color4.DarkGray : Color4.White, 200, Easing.OutQuint), true); } protected override TabItem CreateTabItem(RulesetInfo value) => new DirectRulesetTabItem(value); diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 4b43542b43..8b04bf0387 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Direct protected override Color4 BackgroundColour => OsuColour.FromHex(@"384552"); protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked; + protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard; protected override Drawable CreateSupplementaryControls() => rulesetSelector = new DirectRulesetSelector(); diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 7e33d7ba27..56e93b6a1e 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -15,6 +15,7 @@ namespace osu.Game.Overlays.KeyBinding public GlobalKeyBindingsSection(GlobalActionContainer manager) { Add(new DefaultBindingsSubsection(manager)); + Add(new AudioControlKeyBindingsSubsection(manager)); Add(new InGameKeyBindingsSubsection(manager)); } @@ -39,5 +40,16 @@ namespace osu.Game.Overlays.KeyBinding Defaults = manager.InGameKeyBindings; } } + + private class AudioControlKeyBindingsSubsection : KeyBindingsSubsection + { + protected override string Header => "Audio"; + + public AudioControlKeyBindingsSubsection(GlobalActionContainer manager) + : base(null) + { + Defaults = manager.AudioControlKeyBindings; + } + } } } diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index fa1ee500a8..7b8745cf42 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods } } - foregroundIcon.Highlighted = Selected; + foregroundIcon.Highlighted.Value = Selected; SelectionChanged?.Invoke(SelectedMod); return true; diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 07040f166d..539601c359 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -85,10 +85,7 @@ namespace osu.Game.Overlays.Music private void addBeatmapSet(BeatmapSetInfo obj) => Schedule(() => { - var newItem = new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }; - - items.Add(newItem); - items.SetLayoutPosition(newItem, items.Count - 1); + items.Insert(items.Count - 1, new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }); }); private void removeBeatmapSet(BeatmapSetInfo obj) => Schedule(() => diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index abbcec5094..6ad147735b 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -1,292 +1,121 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; +using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Music; +using osu.Game.Input.Bindings; +using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays { - public class MusicController : OsuFocusedOverlayContainer + /// + /// Handles playback of the global music track. + /// + public class MusicController : Component, IKeyBindingHandler { - private const float player_height = 130; - private const float transition_length = 800; - private const float progress_height = 10; - private const float bottom_black_area_height = 55; - - private Drawable background; - private ProgressBar progressBar; - - private IconButton prevButton; - private IconButton playButton; - private IconButton nextButton; - private IconButton playlistButton; - - private SpriteText title, artist; - - private PlaylistOverlay playlist; - - private BeatmapManager beatmaps; + [Resolved] + private BeatmapManager beatmaps { get; set; } private List beatmapSets; - private Container dragContainer; - private Container playerContainer; - public bool IsUserPaused { get; private set; } + /// + /// Fired when the global has changed. + /// Includes direction information for display purposes. + /// + public event Action TrackChanged; + [Resolved] - private Bindable beatmap { get; set; } + private IBindable beatmap { get; set; } [Resolved] private IBindable> mods { get; set; } - /// - /// Provide a source for the toolbar height. - /// - public Func GetToolbarHeight; - - public MusicController() - { - Width = 400; - Margin = new MarginPadding(10); - } + [Resolved(canBeNull: true)] + private OnScreenDisplay onScreenDisplay { get; set; } [BackgroundDependencyLoader] - private void load(Bindable beatmap, BeatmapManager beatmaps, OsuColour colours) + private void load() { - this.beatmaps = beatmaps; - - Children = new Drawable[] - { - dragContainer = new DragContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - playlist = new PlaylistOverlay - { - RelativeSizeAxes = Axes.X, - Y = player_height + 10, - OrderChanged = playlistOrderChanged - }, - playerContainer = new Container - { - RelativeSizeAxes = Axes.X, - Height = player_height, - Masking = true, - CornerRadius = 5, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }, - Children = new[] - { - background = new Background(), - title = new OsuSpriteText - { - Origin = Anchor.BottomCentre, - Anchor = Anchor.TopCentre, - Position = new Vector2(0, 40), - Font = OsuFont.GetFont(size: 25, italics: true), - Colour = Color4.White, - Text = @"Nothing to play", - }, - artist = new OsuSpriteText - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Position = new Vector2(0, 45), - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold, italics: true), - Colour = Color4.White, - Text = @"Nothing to play", - }, - new Container - { - Padding = new MarginPadding { Bottom = progress_height }, - Height = bottom_black_area_height, - RelativeSizeAxes = Axes.X, - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Children = new[] - { - prevButton = new MusicIconButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = prev, - Icon = FontAwesome.Solid.StepBackward, - }, - playButton = new MusicIconButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.4f), - IconScale = new Vector2(1.4f), - Action = togglePause, - Icon = FontAwesome.Regular.PlayCircle, - }, - nextButton = new MusicIconButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = () => next(), - Icon = FontAwesome.Solid.StepForward, - }, - } - }, - playlistButton = new MusicIconButton - { - Origin = Anchor.Centre, - Anchor = Anchor.CentreRight, - Position = new Vector2(-bottom_black_area_height / 2, 0), - Icon = FontAwesome.Solid.Bars, - Action = () => playlist.ToggleVisibility(), - }, - } - }, - progressBar = new ProgressBar - { - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - Height = progress_height, - FillColour = colours.Yellow, - OnSeek = attemptSeek - } - }, - }, - } - } - }; - beatmapSets = beatmaps.GetAllUsableBeatmapSets(); beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemRemoved += handleBeatmapRemoved; - - playlist.State.ValueChanged += s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } - private ScheduledDelegate seekDelegate; - - private void attemptSeek(double progress) + protected override void LoadComplete() { - seekDelegate?.Cancel(); - seekDelegate = Schedule(() => - { - if (!beatmap.Disabled) - current?.Track.Seek(progress); - }); + beatmap.BindValueChanged(beatmapChanged, true); + mods.BindValueChanged(_ => updateAudioAdjustments(), true); + base.LoadComplete(); } - private void playlistOrderChanged(BeatmapSetInfo beatmapSetInfo, int index) + /// + /// Change the position of a in the current playlist. + /// + /// The beatmap to move. + /// The new position. + public void ChangeBeatmapSetPosition(BeatmapSetInfo beatmapSetInfo, int index) { beatmapSets.Remove(beatmapSetInfo); beatmapSets.Insert(index, beatmapSetInfo); } - private void handleBeatmapAdded(BeatmapSetInfo set) => Schedule(() => beatmapSets.Add(set)); + /// + /// Returns whether the current beatmap track is playing. + /// + public bool IsPlaying => beatmap.Value.Track.IsRunning; - private void handleBeatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); + private void handleBeatmapAdded(BeatmapSetInfo set) => + Schedule(() => beatmapSets.Add(set)); - protected override void LoadComplete() + private void handleBeatmapRemoved(BeatmapSetInfo set) => + Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); + + private ScheduledDelegate seekDelegate; + + public void SeekTo(double position) { - beatmap.BindValueChanged(beatmapChanged, true); - beatmap.BindDisabledChanged(beatmapDisabledChanged, true); - mods.BindValueChanged(_ => updateAudioAdjustments(), true); - base.LoadComplete(); - } - - private void beatmapDisabledChanged(bool disabled) - { - if (disabled) - playlist.Hide(); - - playButton.Enabled.Value = !disabled; - prevButton.Enabled.Value = !disabled; - nextButton.Enabled.Value = !disabled; - playlistButton.Enabled.Value = !disabled; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - Height = dragContainer.Height; - - dragContainer.Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; - } - - protected override void Update() - { - base.Update(); - - if (pendingBeatmapSwitch != null) + seekDelegate?.Cancel(); + seekDelegate = Schedule(() => { - pendingBeatmapSwitch(); - pendingBeatmapSwitch = null; - } - - var track = current?.TrackLoaded ?? false ? current.Track : null; - - if (track?.IsDummyDevice == false) - { - progressBar.EndTime = track.Length; - progressBar.CurrentTime = track.CurrentTime; - - playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; - } - else - { - progressBar.CurrentTime = 0; - progressBar.EndTime = 1; - playButton.Icon = FontAwesome.Regular.PlayCircle; - } + if (!beatmap.Disabled) + current?.Track.Seek(position); + }); } - private void togglePause() + /// + /// Start playing the current track (if not already playing). + /// + public void Play() + { + if (!IsPlaying) + TogglePause(); + } + + /// + /// Toggle pause / play. + /// + /// Whether the operation was successful. + public bool TogglePause() { var track = current?.Track; if (track == null) { - if (!beatmap.Disabled) - next(true); - return; + if (beatmap.Disabled) + return false; + + next(true); + return true; } if (track.IsRunning) @@ -299,48 +128,70 @@ namespace osu.Game.Overlays track.Start(); IsUserPaused = false; } + + return true; } - private void prev() + /// + /// Play the previous track. + /// + /// Whether the operation was successful. + public bool PrevTrack() { - queuedDirection = TransformDirection.Prev; + queuedDirection = TrackChangeDirection.Prev; var playable = beatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? beatmapSets.LastOrDefault(); if (playable != null) { - beatmap.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); + if (beatmap is Bindable working) + working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); beatmap.Value.Track.Restart(); + + return true; } + + return false; } - private void next(bool instant = false) + /// + /// Play the next random or playlist track. + /// + /// Whether the operation was successful. + public bool NextTrack() => next(); + + private bool next(bool instant = false) { if (!instant) - queuedDirection = TransformDirection.Next; + queuedDirection = TrackChangeDirection.Next; var playable = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? beatmapSets.FirstOrDefault(); if (playable != null) { - beatmap.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); + if (beatmap is Bindable working) + working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); beatmap.Value.Track.Restart(); + return true; } + + return false; } private WorkingBeatmap current; - private TransformDirection? queuedDirection; + + private TrackChangeDirection? queuedDirection; private void beatmapChanged(ValueChangedEvent beatmap) { - TransformDirection direction = TransformDirection.None; + TrackChangeDirection direction = TrackChangeDirection.None; if (current != null) { bool audioEquals = beatmap.NewValue?.BeatmapInfo?.AudioEquals(current.BeatmapInfo) ?? false; if (audioEquals) - direction = TransformDirection.None; + direction = TrackChangeDirection.None; else if (queuedDirection.HasValue) { direction = queuedDirection.Value; @@ -352,13 +203,13 @@ namespace osu.Game.Overlays var last = beatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); var next = beatmap.NewValue == null ? -1 : beatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); - direction = last > next ? TransformDirection.Prev : TransformDirection.Next; + direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } } - progressBar.CurrentTime = 0; + current = beatmap.NewValue; + TrackChanged?.Invoke(current, direction); - updateDisplay(current = beatmap.NewValue, direction); updateAudioAdjustments(); queuedDirection = null; @@ -376,167 +227,60 @@ namespace osu.Game.Overlays mod.ApplyToClock(track); } - private Action pendingBeatmapSwitch; - - private void updateDisplay(WorkingBeatmap beatmap, TransformDirection direction) + protected override void Dispose(bool isDisposing) { - // avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps. - pendingBeatmapSwitch = delegate + base.Dispose(isDisposing); + + if (beatmaps != null) { - // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() - Task.Run(() => - { - if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists - { - title.Text = @"Nothing to play"; - artist.Text = @"Nothing to play"; - } - else - { - BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); - artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); - } - }); - - LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground => - { - switch (direction) - { - case TransformDirection.Next: - newBackground.Position = new Vector2(400, 0); - newBackground.MoveToX(0, 500, Easing.OutCubic); - background.MoveToX(-400, 500, Easing.OutCubic); - break; - - case TransformDirection.Prev: - newBackground.Position = new Vector2(-400, 0); - newBackground.MoveToX(0, 500, Easing.OutCubic); - background.MoveToX(400, 500, Easing.OutCubic); - break; - } - - background.Expire(); - background = newBackground; - - playerContainer.Add(newBackground); - }); - }; - } - - protected override void PopIn() - { - base.PopIn(); - - this.FadeIn(transition_length, Easing.OutQuint); - dragContainer.ScaleTo(1, transition_length, Easing.OutElastic); - } - - protected override void PopOut() - { - base.PopOut(); - - this.FadeOut(transition_length, Easing.OutQuint); - dragContainer.ScaleTo(0.9f, transition_length, Easing.OutQuint); - } - - private enum TransformDirection - { - None, - Next, - Prev - } - - private class MusicIconButton : IconButton - { - public MusicIconButton() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - HoverColour = colours.YellowDark.Opacity(0.6f); - FlashColour = colours.Yellow; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - // works with AutoSizeAxes above to make buttons autosize with the scale animation. - Content.AutoSizeAxes = Axes.None; - Content.Size = new Vector2(DEFAULT_BUTTON_SIZE); + beatmaps.ItemAdded -= handleBeatmapAdded; + beatmaps.ItemRemoved -= handleBeatmapRemoved; } } - private class Background : BufferedContainer + public bool OnPressed(GlobalAction action) { - private readonly Sprite sprite; - private readonly WorkingBeatmap beatmap; + if (beatmap.Disabled) + return false; - public Background(WorkingBeatmap beatmap = null) + switch (action) { - this.beatmap = beatmap; - CacheDrawnFrameBuffer = true; - Depth = float.MaxValue; - RelativeSizeAxes = Axes.Both; + case GlobalAction.MusicPlay: + if (TogglePause()) + onScreenDisplay?.Display(new MusicControllerToast(IsPlaying ? "Play track" : "Pause track")); + return true; - Children = new Drawable[] - { - sprite = new Sprite - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(150), - FillMode = FillMode.Fill, - }, - new Box - { - RelativeSizeAxes = Axes.X, - Height = bottom_black_area_height, - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - Colour = Color4.Black.Opacity(0.5f) - } - }; + case GlobalAction.MusicNext: + if (NextTrack()) + onScreenDisplay?.Display(new MusicControllerToast("Next track")); + + return true; + + case GlobalAction.MusicPrev: + if (PrevTrack()) + onScreenDisplay?.Display(new MusicControllerToast("Previous track")); + + return true; } - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); - } + return false; } - private class DragContainer : Container + public bool OnReleased(GlobalAction action) => false; + + public class MusicControllerToast : Toast { - protected override bool OnDragStart(DragStartEvent e) + public MusicControllerToast(string action) + : base("Music Playback", action, string.Empty) { - return true; - } - - protected override bool OnDrag(DragEvent e) - { - Vector2 change = e.MousePosition - e.MouseDownPosition; - - // Diminish the drag distance as we go further to simulate "rubber band" feeling. - change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; - - this.MoveTo(change); - return true; - } - - protected override bool OnDragEnd(DragEndEvent e) - { - this.MoveTo(Vector2.Zero, 800, Easing.OutElastic); - return base.OnDragEnd(e); } } + } - /// - /// Play the next random or playlist track. - /// - public void NextTrack() => next(); + public enum TrackChangeDirection + { + None, + Next, + Prev } } diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index f9278bbbd2..17a2d4cf9f 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -26,8 +26,7 @@ namespace osu.Game.Overlays.Notifications public void Add(Notification notification, float position) { - notifications.Add(notification); - notifications.SetLayoutPosition(notification, position); + notifications.Insert((int)position, notification); } public IEnumerable AcceptTypes; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs new file mode 100644 index 0000000000..a3243a655e --- /dev/null +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -0,0 +1,404 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Music; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays +{ + public class NowPlayingOverlay : OsuFocusedOverlayContainer + { + private const float player_height = 130; + private const float transition_length = 800; + private const float progress_height = 10; + private const float bottom_black_area_height = 55; + + private Drawable background; + private ProgressBar progressBar; + + private IconButton prevButton; + private IconButton playButton; + private IconButton nextButton; + private IconButton playlistButton; + + private SpriteText title, artist; + + private PlaylistOverlay playlist; + + private Container dragContainer; + private Container playerContainer; + + /// + /// Provide a source for the toolbar height. + /// + public Func GetToolbarHeight; + + [Resolved] + private MusicController musicController { get; set; } + + [Resolved] + private Bindable beatmap { get; set; } + + public NowPlayingOverlay() + { + Width = 400; + Margin = new MarginPadding(10); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + dragContainer = new DragContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + playlist = new PlaylistOverlay + { + RelativeSizeAxes = Axes.X, + Y = player_height + 10, + OrderChanged = musicController.ChangeBeatmapSetPosition + }, + playerContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = player_height, + Masking = true, + CornerRadius = 5, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(40), + Radius = 5, + }, + Children = new[] + { + background = new Background(), + title = new OsuSpriteText + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.TopCentre, + Position = new Vector2(0, 40), + Font = OsuFont.GetFont(size: 25, italics: true), + Colour = Color4.White, + Text = @"Nothing to play", + }, + artist = new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Position = new Vector2(0, 45), + Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold, italics: true), + Colour = Color4.White, + Text = @"Nothing to play", + }, + new Container + { + Padding = new MarginPadding { Bottom = progress_height }, + Height = bottom_black_area_height, + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new[] + { + prevButton = new MusicIconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => musicController.PrevTrack(), + Icon = FontAwesome.Solid.StepBackward, + }, + playButton = new MusicIconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.4f), + IconScale = new Vector2(1.4f), + Action = () => musicController.TogglePause(), + Icon = FontAwesome.Regular.PlayCircle, + }, + nextButton = new MusicIconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => musicController.NextTrack(), + Icon = FontAwesome.Solid.StepForward, + }, + } + }, + playlistButton = new MusicIconButton + { + Origin = Anchor.Centre, + Anchor = Anchor.CentreRight, + Position = new Vector2(-bottom_black_area_height / 2, 0), + Icon = FontAwesome.Solid.Bars, + Action = () => playlist.ToggleVisibility(), + }, + } + }, + progressBar = new ProgressBar + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Height = progress_height, + FillColour = colours.Yellow, + OnSeek = musicController.SeekTo + } + }, + }, + } + } + }; + + playlist.State.ValueChanged += s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindDisabledChanged(beatmapDisabledChanged, true); + + musicController.TrackChanged += trackChanged; + trackChanged(beatmap.Value); + } + + protected override void PopIn() + { + base.PopIn(); + + this.FadeIn(transition_length, Easing.OutQuint); + dragContainer.ScaleTo(1, transition_length, Easing.OutElastic); + } + + protected override void PopOut() + { + base.PopOut(); + + this.FadeOut(transition_length, Easing.OutQuint); + dragContainer.ScaleTo(0.9f, transition_length, Easing.OutQuint); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + Height = dragContainer.Height; + dragContainer.Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; + } + + protected override void Update() + { + base.Update(); + + if (pendingBeatmapSwitch != null) + { + pendingBeatmapSwitch(); + pendingBeatmapSwitch = null; + } + + var track = beatmap.Value?.TrackLoaded ?? false ? beatmap.Value.Track : null; + + if (track?.IsDummyDevice == false) + { + progressBar.EndTime = track.Length; + progressBar.CurrentTime = track.CurrentTime; + + playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + } + else + { + progressBar.CurrentTime = 0; + progressBar.EndTime = 1; + playButton.Icon = FontAwesome.Regular.PlayCircle; + } + } + + private Action pendingBeatmapSwitch; + + private void trackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction = TrackChangeDirection.None) + { + // avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps. + pendingBeatmapSwitch = delegate + { + // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() + Task.Run(() => + { + if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists + { + title.Text = @"Nothing to play"; + artist.Text = @"Nothing to play"; + } + else + { + BeatmapMetadata metadata = beatmap.Metadata; + title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); + artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); + } + }); + + LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground => + { + switch (direction) + { + case TrackChangeDirection.Next: + newBackground.Position = new Vector2(400, 0); + newBackground.MoveToX(0, 500, Easing.OutCubic); + background.MoveToX(-400, 500, Easing.OutCubic); + break; + + case TrackChangeDirection.Prev: + newBackground.Position = new Vector2(-400, 0); + newBackground.MoveToX(0, 500, Easing.OutCubic); + background.MoveToX(400, 500, Easing.OutCubic); + break; + } + + background.Expire(); + background = newBackground; + + playerContainer.Add(newBackground); + }); + }; + } + + private void beatmapDisabledChanged(bool disabled) + { + if (disabled) + playlist.Hide(); + + playButton.Enabled.Value = !disabled; + prevButton.Enabled.Value = !disabled; + nextButton.Enabled.Value = !disabled; + playlistButton.Enabled.Value = !disabled; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (musicController != null) + musicController.TrackChanged -= trackChanged; + } + + private class MusicIconButton : IconButton + { + public MusicIconButton() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + HoverColour = colours.YellowDark.Opacity(0.6f); + FlashColour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // works with AutoSizeAxes above to make buttons autosize with the scale animation. + Content.AutoSizeAxes = Axes.None; + Content.Size = new Vector2(DEFAULT_BUTTON_SIZE); + } + } + + private class Background : BufferedContainer + { + private readonly Sprite sprite; + private readonly WorkingBeatmap beatmap; + + public Background(WorkingBeatmap beatmap = null) + { + this.beatmap = beatmap; + CacheDrawnFrameBuffer = true; + Depth = float.MaxValue; + RelativeSizeAxes = Axes.Both; + + Children = new Drawable[] + { + sprite = new Sprite + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(150), + FillMode = FillMode.Fill, + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = bottom_black_area_height, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Colour = Color4.Black.Opacity(0.5f) + } + }; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); + } + } + + private class DragContainer : Container + { + protected override bool OnDragStart(DragStartEvent e) + { + return true; + } + + protected override bool OnDrag(DragEvent e) + { + Vector2 change = e.MousePosition - e.MouseDownPosition; + + // Diminish the drag distance as we go further to simulate "rubber band" feeling. + change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; + + this.MoveTo(change); + return true; + } + + protected override bool OnDragEnd(DragEndEvent e) + { + this.MoveTo(Vector2.Zero, 800, Easing.OutElastic); + return base.OnDragEnd(e); + } + } + } +} diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs new file mode 100644 index 0000000000..46c53ec409 --- /dev/null +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.OSD +{ + public abstract class Toast : Container + { + private const int toast_minimum_width = 240; + + private readonly Container content; + protected override Container Content => content; + + protected readonly OsuSpriteText ValueText; + + protected Toast(string description, string value, string shortcut) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + // A toast's height is decided (and transformed) by the containing OnScreenDisplay. + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + InternalChildren = new Drawable[] + { + new Container //this container exists just to set a minimum width for the toast + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = toast_minimum_width + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.7f + }, + content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Padding = new MarginPadding(10), + Name = "Description", + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Black), + Spacing = new Vector2(1, 0), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = description.ToUpperInvariant() + }, + ValueText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), + Padding = new MarginPadding { Left = 10, Right = 10 }, + Name = "Value", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = value + }, + new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Name = "Shortcut", + Alpha = 0.3f, + Margin = new MarginPadding { Bottom = 15 }, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant() + }, + }; + } + } +} diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs new file mode 100644 index 0000000000..8e8a99a0a7 --- /dev/null +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -0,0 +1,147 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Configuration.Tracking; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.OSD +{ + public class TrackedSettingToast : Toast + { + private const int lights_bottom_margin = 40; + + public TrackedSettingToast(SettingDescription description) + : base(description.Name, description.Value, description.Shortcut) + { + FillFlowContainer optionLights; + + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding { Bottom = lights_bottom_margin }, + Children = new Drawable[] + { + optionLights = new FillFlowContainer + { + Margin = new MarginPadding { Bottom = 5 }, + Spacing = new Vector2(5, 0), + Direction = FillDirection.Horizontal, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both + }, + } + } + }; + + int optionCount = 0; + int selectedOption = -1; + + switch (description.RawValue) + { + case bool val: + optionCount = 1; + if (val) selectedOption = 0; + break; + + case Enum _: + var values = Enum.GetValues(description.RawValue.GetType()); + optionCount = values.Length; + selectedOption = Convert.ToInt32(description.RawValue); + break; + } + + ValueText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre; + + for (int i = 0; i < optionCount; i++) + optionLights.Add(new OptionLight { Glowing = i == selectedOption }); + } + + private class OptionLight : Container + { + private Color4 glowingColour, idleColour; + + private const float transition_speed = 300; + + private const float glow_strength = 0.4f; + + private readonly Box fill; + + public OptionLight() + { + Children = new[] + { + fill = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 1, + }, + }; + } + + private bool glowing; + + public bool Glowing + { + set + { + glowing = value; + if (!IsLoaded) return; + + updateGlow(); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + fill.Colour = idleColour = Color4.White.Opacity(0.4f); + glowingColour = Color4.White; + + Size = new Vector2(25, 5); + + Masking = true; + CornerRadius = 3; + + EdgeEffect = new EdgeEffectParameters + { + Colour = colours.BlueDark.Opacity(glow_strength), + Type = EdgeEffectType.Glow, + Radius = 8, + }; + } + + protected override void LoadComplete() + { + updateGlow(); + FinishTransforms(true); + } + + private void updateGlow() + { + if (glowing) + { + fill.FadeColour(glowingColour, transition_speed, Easing.OutQuint); + FadeEdgeEffectTo(glow_strength, transition_speed, Easing.OutQuint); + } + else + { + FadeEdgeEffectTo(0, transition_speed, Easing.OutQuint); + fill.FadeColour(idleColour, transition_speed, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index 88a1edddc5..a92320945e 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -8,34 +8,25 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Transforms; using osu.Framework.Threading; using osu.Game.Configuration; -using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.OSD; namespace osu.Game.Overlays { + /// + /// An on-screen display which automatically tracks and displays toast notifications for . + /// Can also display custom content via + /// public class OnScreenDisplay : Container { private readonly Container box; - private readonly SpriteText textLine1; - private readonly SpriteText textLine2; - private readonly SpriteText textLine3; - private const float height = 110; - private const float height_notext = 98; private const float height_contracted = height * 0.9f; - private readonly FillFlowContainer optionLights; - public OnScreenDisplay() { RelativeSizeAxes = Axes.Both; @@ -52,64 +43,6 @@ namespace osu.Game.Overlays Height = height_contracted, Alpha = 0, CornerRadius = 20, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.7f, - }, - new Container // purely to add a minimum width - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 240, - RelativeSizeAxes = Axes.Y, - }, - textLine1 = new OsuSpriteText - { - Padding = new MarginPadding(10), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Black), - Spacing = new Vector2(1, 0), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - textLine2 = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), - Padding = new MarginPadding { Left = 10, Right = 10 }, - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - optionLights = new FillFlowContainer - { - Padding = new MarginPadding { Top = 20, Bottom = 5 }, - Spacing = new Vector2(5, 0), - Direction = FillDirection.Horizontal, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both - }, - textLine3 = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Bottom = 15 }, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Alpha = 0.3f, - }, - } - } - } }, }; } @@ -142,7 +75,7 @@ namespace osu.Game.Overlays return; configManager.LoadInto(trackedSettings); - trackedSettings.SettingChanged += display; + trackedSettings.SettingChanged += displayTrackedSettingChange; trackedConfigManagers.Add((source, configManager), trackedSettings); } @@ -162,56 +95,23 @@ namespace osu.Game.Overlays return; existing.Unload(); - existing.SettingChanged -= display; + existing.SettingChanged -= displayTrackedSettingChange; trackedConfigManagers.Remove((source, configManager)); } - private void display(SettingDescription description) + /// + /// Displays the provided temporarily. + /// + /// + public void Display(Toast toast) { - Schedule(() => - { - textLine1.Text = description.Name.ToUpperInvariant(); - textLine2.Text = description.Value; - textLine3.Text = description.Shortcut.ToUpperInvariant(); - - if (string.IsNullOrEmpty(textLine3.Text)) - textLine3.Text = "NO KEY BOUND"; - - DisplayTemporarily(box); - - int optionCount = 0; - int selectedOption = -1; - - switch (description.RawValue) - { - case bool val: - optionCount = 1; - if (val) selectedOption = 0; - break; - - case Enum _: - var values = Enum.GetValues(description.RawValue.GetType()); - optionCount = values.Length; - selectedOption = Convert.ToInt32(description.RawValue); - break; - } - - textLine2.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre; - textLine2.Y = optionCount > 0 ? 0 : 5; - - if (optionLights.Children.Count != optionCount) - { - optionLights.Clear(); - for (int i = 0; i < optionCount; i++) - optionLights.Add(new OptionLight()); - } - - for (int i = 0; i < optionCount; i++) - optionLights.Children[i].Glowing = i == selectedOption; - }); + box.Child = toast; + DisplayTemporarily(box); } + private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description))); + private TransformSequence fadeIn; private ScheduledDelegate fadeOut; @@ -236,80 +136,5 @@ namespace osu.Game.Overlays b => b.ResizeHeightTo(height_contracted, 1500, Easing.InQuint)); }, 500); } - - private class OptionLight : Container - { - private Color4 glowingColour, idleColour; - - private const float transition_speed = 300; - - private const float glow_strength = 0.4f; - - private readonly Box fill; - - public OptionLight() - { - Children = new[] - { - fill = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 1, - }, - }; - } - - private bool glowing; - - public bool Glowing - { - set - { - glowing = value; - if (!IsLoaded) return; - - updateGlow(); - } - } - - private void updateGlow() - { - if (glowing) - { - fill.FadeColour(glowingColour, transition_speed, Easing.OutQuint); - FadeEdgeEffectTo(glow_strength, transition_speed, Easing.OutQuint); - } - else - { - FadeEdgeEffectTo(0, transition_speed, Easing.OutQuint); - fill.FadeColour(idleColour, transition_speed, Easing.OutQuint); - } - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - fill.Colour = idleColour = Color4.White.Opacity(0.4f); - glowingColour = Color4.White; - - Size = new Vector2(25, 5); - - Masking = true; - CornerRadius = 3; - - EdgeEffect = new EdgeEffectParameters - { - Colour = colours.BlueDark.Opacity(glow_strength), - Type = EdgeEffectType.Glow, - Radius = 8, - }; - } - - protected override void LoadComplete() - { - updateGlow(); - FinishTransforms(true); - } - } } } diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index e7f7c2f490..158641d816 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -140,6 +140,9 @@ namespace osu.Game.Overlays.Profile.Header { if (string.IsNullOrEmpty(content)) return; + // newlines could be contained in API returned user content. + content = content.Replace("\n", " "); + bottomLinkContainer.AddIcon(icon, text => { text.Font = text.Font.With(size: 10); diff --git a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs index 2e4fd6fe3d..6e1b6e2c7d 100644 --- a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs @@ -53,6 +53,6 @@ namespace osu.Game.Overlays.Profile.Header.Components User.BindValueChanged(user => updateFollowers(user.NewValue), true); } - private void updateFollowers(User user) => followerText.Text = user?.FollowerCount?.Length > 0 ? user.FollowerCount[0].ToString("#,##0") : "0"; + private void updateFollowers(User user) => followerText.Text = user?.FollowerCount.ToString("#,##0"); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs new file mode 100644 index 0000000000..f18f319e27 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernames.cs @@ -0,0 +1,168 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public class PreviousUsernames : CompositeDrawable + { + private const int duration = 200; + private const int margin = 10; + private const int width = 310; + private const int move_offset = 15; + + public readonly Bindable User = new Bindable(); + + private readonly TextFlowContainer text; + private readonly Box background; + private readonly SpriteText header; + + public PreviousUsernames() + { + HoverIconContainer hoverIcon; + + AutoSizeAxes = Axes.Y; + Width = width; + Masking = true; + CornerRadius = 5; + + AddRangeInternal(new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Distributed) + }, + Content = new[] + { + new Drawable[] + { + hoverIcon = new HoverIconContainer(), + header = new SpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = @"formerly known as", + Font = OsuFont.GetFont(size: 10, italics: true) + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + }, + text = new TextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Margin = new MarginPadding { Bottom = margin, Top = margin / 2f } + } + } + } + } + }); + + hoverIcon.ActivateHover += showContent; + hideContent(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.GreySeafoamDarker; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserChanged, true); + } + + private void onUserChanged(ValueChangedEvent user) + { + text.Text = string.Empty; + + var usernames = user.NewValue?.PreviousUsernames; + + if (usernames?.Any() ?? false) + { + text.Text = string.Join(", ", usernames); + Show(); + return; + } + + Hide(); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + hideContent(); + } + + private void showContent() + { + text.FadeIn(duration, Easing.OutQuint); + header.FadeIn(duration, Easing.OutQuint); + background.FadeIn(duration, Easing.OutQuint); + this.MoveToY(-move_offset, duration, Easing.OutQuint); + } + + private void hideContent() + { + text.FadeOut(duration, Easing.OutQuint); + header.FadeOut(duration, Easing.OutQuint); + background.FadeOut(duration, Easing.OutQuint); + this.MoveToY(0, duration, Easing.OutQuint); + } + + private class HoverIconContainer : Container + { + public Action ActivateHover; + + public HoverIconContainer() + { + AutoSizeAxes = Axes.Both; + Child = new SpriteIcon + { + Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 }, + Size = new Vector2(15), + Icon = FontAwesome.Solid.IdCard, + }; + } + + protected override bool OnHover(HoverEvent e) + { + ActivateHover?.Invoke(); + return base.OnHover(e); + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs index b6112a6501..2c9a3dd5f9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileRulesetSelector.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Rulesets; +using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -16,6 +18,8 @@ namespace osu.Game.Overlays.Profile.Header.Components { private Color4 accentColour = Color4.White; + public readonly Bindable User = new Bindable(); + public ProfileRulesetSelector() { TabContainer.Masking = false; @@ -32,24 +36,17 @@ namespace osu.Game.Overlays.Profile.Header.Components ((ProfileRulesetTabItem)tabItem).AccentColour = accentColour; } - public void SetDefaultRuleset(RulesetInfo ruleset) + protected override void LoadComplete() { - // Todo: This method shouldn't exist, but bindables don't provide the concept of observing a change to the default value - foreach (TabItem tabItem in TabContainer) - ((ProfileRulesetTabItem)tabItem).IsDefault = ((ProfileRulesetTabItem)tabItem).Value.ID == ruleset.ID; + base.LoadComplete(); + + User.BindValueChanged(u => SetDefaultRuleset(Rulesets.GetRuleset(u.NewValue?.PlayMode ?? "osu")), true); } - public void SelectDefaultRuleset() + public void SetDefaultRuleset(RulesetInfo ruleset) { - // Todo: This method shouldn't exist, but bindables don't provide the concept of observing a change to the default value foreach (TabItem tabItem in TabContainer) - { - if (((ProfileRulesetTabItem)tabItem).IsDefault) - { - Current.Value = ((ProfileRulesetTabItem)tabItem).Value; - return; - } - } + ((ProfileRulesetTabItem)tabItem).IsDefault = ((ProfileRulesetTabItem)tabItem).Value.ID == ruleset.ID; } protected override TabItem CreateTabItem(RulesetInfo value) => new ProfileRulesetTabItem(value) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 5f79386b76..24ed0cc022 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private KeyValuePair[] ranks; private int dayIndex; - public Bindable User = new Bindable(); + public readonly Bindable Statistics = new Bindable(); public RankGraph() { @@ -56,8 +56,6 @@ namespace osu.Game.Overlays.Profile.Header.Components }; graph.OnBallMove += i => dayIndex = i; - - User.ValueChanged += userChanged; } [BackgroundDependencyLoader] @@ -66,18 +64,25 @@ namespace osu.Game.Overlays.Profile.Header.Components graph.LineColour = colours.Yellow; } - private void userChanged(ValueChangedEvent e) + protected override void LoadComplete() + { + base.LoadComplete(); + + Statistics.BindValueChanged(statistics => updateStatistics(statistics.NewValue), true); + } + + private void updateStatistics(UserStatistics statistics) { placeholder.FadeIn(fade_duration, Easing.Out); - if (e.NewValue?.Statistics?.Ranks.Global == null) + if (statistics?.Ranks.Global == null) { graph.FadeOut(fade_duration, Easing.Out); ranks = null; return; } - int[] userRanks = e.NewValue.RankHistory?.Data ?? new[] { e.NewValue.Statistics.Ranks.Global.Value }; + int[] userRanks = statistics.RankHistory?.Data ?? new[] { statistics.Ranks.Global.Value }; ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); if (ranks.Length > 1) @@ -191,17 +196,30 @@ namespace osu.Game.Overlays.Profile.Header.Components } } - public string TooltipText => User.Value?.Statistics?.Ranks.Global == null ? "" : $"#{ranks[dayIndex].Value:#,##0}|{ranked_days - ranks[dayIndex].Key + 1}"; + public object TooltipContent + { + get + { + if (Statistics.Value?.Ranks.Global == null) + return null; + + var days = ranked_days - ranks[dayIndex].Key + 1; + + return new TooltipDisplayContent + { + Rank = $"#{ranks[dayIndex].Value:#,##0}", + Time = days == 0 ? "now" : $"{days} days ago" + }; + } + } public ITooltip GetCustomTooltip() => new RankGraphTooltip(); - public class RankGraphTooltip : VisibilityContainer, ITooltip + private class RankGraphTooltip : VisibilityContainer, ITooltip { private readonly OsuSpriteText globalRankingText, timeText; private readonly Box background; - public string TooltipText { get; set; } - public RankGraphTooltip() { AutoSizeAxes = Axes.Both; @@ -255,11 +273,14 @@ namespace osu.Game.Overlays.Profile.Header.Components background.Colour = colours.GreySeafoamDark; } - public void Refresh() + public bool SetContent(object content) { - var info = TooltipText.Split('|'); - globalRankingText.Text = info[0]; - timeText.Text = info[1] == "0" ? "now" : $"{info[1]} days ago"; + if (!(content is TooltipDisplayContent info)) + return false; + + globalRankingText.Text = info.Rank; + timeText.Text = info.Time; + return true; } private bool instantMove = true; @@ -279,5 +300,11 @@ namespace osu.Game.Overlays.Profile.Header.Components protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); } + + private class TooltipDisplayContent + { + public string Rank; + public string Time; + } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs index c5e61f68f4..fa60a37ddb 100644 --- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs +++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs @@ -80,7 +80,6 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load(OsuColour colours) { background.Colour = colours.Pink; - iconContainer.Colour = colours.GreySeafoam; } } } diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index 0db1cb32d7..6ee0d9ee8f 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -179,7 +179,7 @@ namespace osu.Game.Overlays.Profile.Header detailGlobalRank.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-"; detailCountryRank.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; - rankGraph.User.Value = user; + rankGraph.Statistics.Value = user?.Statistics; } private class ScoreRankInfo : CompositeDrawable diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs index 67229a80c0..45bc60f794 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs @@ -78,10 +78,8 @@ namespace osu.Game.Overlays.Profile.Header int displayIndex = index; LoadComponentAsync(new DrawableBadge(badges[index]), asyncBadge => { - badgeFlowContainer.Add(asyncBadge); - // load in stable order regardless of async load order. - badgeFlowContainer.SetLayoutPosition(asyncBadge, displayIndex); + badgeFlowContainer.Insert(displayIndex, asyncBadge); }); } } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs index 16326900f1..13b547eed3 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs @@ -4,26 +4,23 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; namespace osu.Game.Overlays.Profile.Sections { /// /// Display artist/title/mapper information, commonly used as the left portion of a profile or score display row (see ). /// - public class BeatmapMetadataContainer : OsuHoverContainer + public abstract class BeatmapMetadataContainer : OsuHoverContainer { private readonly BeatmapInfo beatmap; - public BeatmapMetadataContainer(BeatmapInfo beatmap) + protected BeatmapMetadataContainer(BeatmapInfo beatmap) { this.beatmap = beatmap; + AutoSizeAxes = Axes.Both; - TooltipText = $"{beatmap.Metadata.Artist} - {beatmap.Metadata.Title}"; } [BackgroundDependencyLoader(true)] @@ -40,23 +37,10 @@ namespace osu.Game.Overlays.Profile.Sections Child = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = new LocalisedString(( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ")), - Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold, italics: true) - }, - new OsuSpriteText - { - Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), - Padding = new MarginPadding { Top = 3 }, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular, italics: true) - }, - }, + Children = CreateText(beatmap), }; } + + protected abstract Drawable[] CreateText(BeatmapInfo beatmap); } } diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index b6b0e605d7..919f8a2fa0 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -1,21 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Direct; using osu.Game.Users; using osuTK; namespace osu.Game.Overlays.Profile.Sections.Beatmaps { - public class PaginatedBeatmapContainer : PaginatedContainer + public class PaginatedBeatmapContainer : PaginatedContainer { private const float panel_padding = 10f; private readonly BeatmapSetType type; - private GetUserBeatmapsRequest request; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string header, string missing = "None... yet.") : base(user, header, missing) @@ -27,37 +28,15 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps ItemsContainer.Spacing = new Vector2(panel_padding); } - protected override void ShowMore() - { - request = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); - request.Success += sets => Schedule(() => + protected override APIRequest> CreateRequest() => + new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); + + protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue + ? null + : new DirectGridPanel(model.ToBeatmapSet(Rulesets)) { - MoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0); - MoreButton.IsLoading = false; - - if (!sets.Any() && VisiblePages == 1) - { - MissingText.Show(); - return; - } - - foreach (var s in sets) - { - if (!s.OnlineBeatmapSetID.HasValue) - continue; - - var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets)); - ItemsContainer.Add(panel); - } - }); - - Api.Queue(request); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - request?.Cancel(); - } + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }; } } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index ae91837d29..37f017277f 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections { new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, "Favourite Beatmaps"), new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, "Ranked & Approved Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, "Loved Beatmaps"), new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps"), }; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs new file mode 100644 index 0000000000..0206c4e13b --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -0,0 +1,182 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using System.Collections.Generic; +using osu.Framework.Graphics.Cursor; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class DrawableMostPlayedBeatmap : OsuHoverContainer + { + private const int cover_width = 100; + private const int corner_radius = 6; + private const int height = 50; + + private readonly BeatmapInfo beatmap; + private readonly int playCount; + + private Box background; + + protected override IEnumerable EffectTargets => new[] { background }; + + public DrawableMostPlayedBeatmap(BeatmapInfo beatmap, int playCount) + { + this.beatmap = beatmap; + this.playCount = playCount; + Enabled.Value = true; //manually enabled, because we have no action + + RelativeSizeAxes = Axes.X; + Height = height; + + Masking = true; + CornerRadius = corner_radius; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + IdleColour = colours.GreySeafoam; + HoverColour = colours.GreySeafoamLight; + + Children = new Drawable[] + { + new UpdateableBeatmapSetCover + { + RelativeSizeAxes = Axes.Y, + Width = cover_width, + BeatmapSet = beatmap.BeatmapSet, + CoverType = BeatmapSetCoverType.List, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = cover_width - corner_radius }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] + { + background = new Box { RelativeSizeAxes = Axes.Both }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new MostPlayedBeatmapMetadataContainer(beatmap), + new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular)) + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Colour = colours.GreySeafoamLighter + }.With(d => + { + d.AddText("mapped by "); + d.AddUserLink(beatmap.Metadata.Author); + }), + } + }, + new PlayCountText(playCount) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight + }, + } + }, + } + } + } + } + }; + } + + private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer + { + public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmap) + : base(beatmap) + { + } + + protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + { + new OsuSpriteText + { + Text = new LocalisedString(( + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{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)), + Font = OsuFont.GetFont(weight: FontWeight.Regular) + }, + }; + } + + private class PlayCountText : CompositeDrawable, IHasTooltip + { + public string TooltipText => "times played"; + + public PlayCountText(int playCount) + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(12), + Icon = FontAwesome.Solid.Play, + }, + new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Text = playCount.ToString(), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular), + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs deleted file mode 100644 index 1b286f92d3..0000000000 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osuTK; - -namespace osu.Game.Overlays.Profile.Sections.Historical -{ - public class DrawableMostPlayedRow : DrawableProfileRow - { - private readonly BeatmapInfo beatmap; - private readonly int playCount; - - public DrawableMostPlayedRow(BeatmapInfo beatmap, int playCount) - { - this.beatmap = beatmap; - this.playCount = playCount; - } - - protected override Drawable CreateLeftVisual() => new UpdateableBeatmapSetCover - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(80, 50), - BeatmapSet = beatmap.BeatmapSet, - CoverType = BeatmapSetCoverType.List, - }; - - [BackgroundDependencyLoader] - private void load() - { - LeftFlowContainer.Add(new BeatmapMetadataContainer(beatmap)); - LeftFlowContainer.Add(new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 12)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - }.With(d => - { - d.AddText("mapped by "); - d.AddUserLink(beatmap.Metadata.Author); - })); - - RightFlowContainer.Add(new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new[] - { - new OsuSpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Text = playCount.ToString(), - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold, italics: true) - }, - new OsuSpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Text = @"times played ", - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular, italics: true) - }, - } - }); - } - } -} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 6085b0bc05..6e6d6272c7 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -1,19 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections.Historical { - public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer + public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { - private GetUserMostPlayedBeatmapsRequest request; - public PaginatedMostPlayedBeatmapContainer(Bindable user) : base(user, "Most Played Beatmaps", "No records. :(") { @@ -22,35 +22,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical ItemsContainer.Direction = FillDirection.Vertical; } - protected override void ShowMore() - { - request = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); - request.Success += beatmaps => Schedule(() => - { - MoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0); - MoreButton.IsLoading = false; + protected override APIRequest> CreateRequest() => + new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage); - if (!beatmaps.Any() && VisiblePages == 1) - { - MissingText.Show(); - return; - } - - MissingText.Hide(); - - foreach (var beatmap in beatmaps) - { - ItemsContainer.Add(new DrawableMostPlayedRow(beatmap.GetBeatmapInfo(Rulesets), beatmap.PlayCount)); - } - }); - - Api.Queue(request); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - request?.Cancel(); - } + protected override Drawable CreateDrawableItem(APIUserMostPlayedBeatmap model) => + new DrawableMostPlayedBeatmap(model.GetBeatmapInfo(Rulesets), model.PlayCount); } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index b459afcb49..bb221bd43a 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -11,22 +11,27 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Users; +using System.Collections.Generic; +using System.Linq; +using System.Threading; namespace osu.Game.Overlays.Profile.Sections { - public abstract class PaginatedContainer : FillFlowContainer + public abstract class PaginatedContainer : FillFlowContainer { - protected readonly FillFlowContainer ItemsContainer; - protected readonly ShowMoreButton MoreButton; - protected readonly OsuSpriteText MissingText; + private readonly ShowMoreButton moreButton; + private readonly OsuSpriteText missingText; + private APIRequest> retrievalRequest; + private CancellationTokenSource loadCancellation; + + [Resolved] + private IAPIProvider api { get; set; } protected int VisiblePages; protected int ItemsPerPage; protected readonly Bindable User = new Bindable(); - - protected IAPIProvider Api; - protected APIRequest RetrievalRequest; + protected readonly FillFlowContainer ItemsContainer; protected RulesetStore Rulesets; protected PaginatedContainer(Bindable user, string header, string missing) @@ -51,15 +56,15 @@ namespace osu.Game.Overlays.Profile.Sections RelativeSizeAxes = Axes.X, Spacing = new Vector2(0, 2), }, - MoreButton = new ShowMoreButton + moreButton = new ShowMoreButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Alpha = 0, Margin = new MarginPadding { Top = 10 }, - Action = ShowMore, + Action = showMore, }, - MissingText = new OsuSpriteText + missingText = new OsuSpriteText { Font = OsuFont.GetFont(size: 15), Text = missing, @@ -69,9 +74,8 @@ namespace osu.Game.Overlays.Profile.Sections } [BackgroundDependencyLoader] - private void load(IAPIProvider api, RulesetStore rulesets) + private void load(RulesetStore rulesets) { - Api = api; Rulesets = rulesets; User.ValueChanged += onUserChanged; @@ -80,13 +84,54 @@ namespace osu.Game.Overlays.Profile.Sections private void onUserChanged(ValueChangedEvent e) { + loadCancellation?.Cancel(); + retrievalRequest?.Cancel(); + VisiblePages = 0; ItemsContainer.Clear(); if (e.NewValue != null) - ShowMore(); + showMore(); } - protected abstract void ShowMore(); + private void showMore() + { + loadCancellation = new CancellationTokenSource(); + + retrievalRequest = CreateRequest(); + retrievalRequest.Success += UpdateItems; + + api.Queue(retrievalRequest); + } + + protected virtual void UpdateItems(List items) => Schedule(() => + { + if (!items.Any() && VisiblePages == 1) + { + moreButton.Hide(); + moreButton.IsLoading = false; + missingText.Show(); + return; + } + + LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null), drawables => + { + missingText.Hide(); + moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0); + moreButton.IsLoading = false; + + ItemsContainer.AddRange(drawables); + }, loadCancellation.Token); + }); + + protected abstract APIRequest> CreateRequest(); + + protected abstract Drawable CreateDrawableItem(TModel model); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + retrievalRequest?.Cancel(); + } } } diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 0a90c9b135..e54ce44ca2 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -10,6 +10,8 @@ using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osu.Game.Scoring; +using osu.Game.Beatmaps; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections.Ranks { @@ -49,10 +51,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Font = OsuFont.GetFont(size: 11, weight: FontWeight.Regular, italics: true) }; - RightFlowContainer.Add(text); - RightFlowContainer.SetLayoutPosition(text, 1); + RightFlowContainer.Insert(1, text); - LeftFlowContainer.Add(new BeatmapMetadataContainer(Score.Beatmap)); + LeftFlowContainer.Add(new ProfileScoreBeatmapMetadataContainer(Score.Beatmap)); LeftFlowContainer.Add(new DrawableDate(Score.Date)); foreach (Mod mod in Score.Mods) @@ -65,5 +66,30 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Width = 60, FillMode = FillMode.Fit, }; + + private class ProfileScoreBeatmapMetadataContainer : BeatmapMetadataContainer + { + public ProfileScoreBeatmapMetadataContainer(BeatmapInfo beatmap) + : base(beatmap) + { + } + + protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + { + new OsuSpriteText + { + Text = new LocalisedString(( + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ")), + Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold, italics: true) + }, + new OsuSpriteText + { + Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), + Padding = new MarginPadding { Top = 3 }, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular, italics: true) + }, + }; + } } } diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index a149cfa12e..853b9db0a7 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -5,18 +5,18 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests; using osu.Game.Users; using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using System.Collections.Generic; +using osu.Game.Online.API; namespace osu.Game.Overlays.Profile.Sections.Ranks { - public class PaginatedScoreContainer : PaginatedContainer + public class PaginatedScoreContainer : PaginatedContainer { private readonly bool includeWeight; private readonly ScoreType type; - private GetUserScoresRequest request; public PaginatedScoreContainer(ScoreType type, Bindable user, string header, string missing, bool includeWeight = false) : base(user, header, missing) @@ -29,52 +29,27 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks ItemsContainer.Direction = FillDirection.Vertical; } - protected override void ShowMore() + protected override void UpdateItems(List items) { - request = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); - request.Success += scores => Schedule(() => - { - foreach (var s in scores) - s.Ruleset = Rulesets.GetRuleset(s.RulesetID); + foreach (var item in items) + item.Ruleset = Rulesets.GetRuleset(item.RulesetID); - if (!scores.Any() && VisiblePages == 1) - { - MoreButton.Hide(); - MoreButton.IsLoading = false; - MissingText.Show(); - return; - } - - IEnumerable drawableScores; - - switch (type) - { - default: - drawableScores = scores.Select(score => new DrawablePerformanceScore(score, includeWeight ? Math.Pow(0.95, ItemsContainer.Count) : (double?)null)); - break; - - case ScoreType.Recent: - drawableScores = scores.Select(score => new DrawableTotalScore(score)); - break; - } - - LoadComponentsAsync(drawableScores, s => - { - MissingText.Hide(); - MoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0); - MoreButton.IsLoading = false; - - ItemsContainer.AddRange(s); - }); - }); - - Api.Queue(request); + base.UpdateItems(items); } - protected override void Dispose(bool isDisposing) + protected override APIRequest> CreateRequest() => + new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); + + protected override Drawable CreateDrawableItem(APILegacyScoreInfo model) { - base.Dispose(isDisposing); - request?.Cancel(); + switch (type) + { + default: + return new DrawablePerformanceScore(model, includeWeight ? Math.Pow(0.95, ItemsContainer.Count) : (double?)null); + + case ScoreType.Recent: + return new DrawableTotalScore(model); + } } } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index b72aec7a44..3f9d4dc93e 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -4,51 +4,24 @@ using osu.Framework.Graphics; using osu.Game.Online.API.Requests; using osu.Game.Users; -using System.Linq; using osu.Framework.Bindables; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.API; +using System.Collections.Generic; namespace osu.Game.Overlays.Profile.Sections.Recent { - public class PaginatedRecentActivityContainer : PaginatedContainer + public class PaginatedRecentActivityContainer : PaginatedContainer { - private GetUserRecentActivitiesRequest request; - public PaginatedRecentActivityContainer(Bindable user, string header, string missing) : base(user, header, missing) { ItemsPerPage = 5; } - protected override void ShowMore() - { - request = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); - request.Success += activities => Schedule(() => - { - MoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); - MoreButton.IsLoading = false; + protected override APIRequest> CreateRequest() => + new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++, ItemsPerPage); - if (!activities.Any() && VisiblePages == 1) - { - MissingText.Show(); - return; - } - - MissingText.Hide(); - - foreach (APIRecentActivity activity in activities) - { - ItemsContainer.Add(new DrawableRecentActivity(activity)); - } - }); - - Api.Queue(request); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - request?.Cancel(); - } + protected override Drawable CreateDrawableItem(APIRecentActivity model) => new DrawableRecentActivity(model); } } diff --git a/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs index 5ed546c62b..cf4e1c0dde 100644 --- a/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs +++ b/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs @@ -124,14 +124,12 @@ namespace osu.Game.Overlays.Profile.Sections private class ChevronIcon : SpriteIcon { - private const int bottom_margin = 2; private const int icon_size = 8; public ChevronIcon() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - Margin = new MarginPadding { Bottom = bottom_margin }; Size = new Vector2(icon_size); Icon = FontAwesome.Solid.ChevronDown; } diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index b0a8a0e77d..a0c4e9a080 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -26,6 +26,7 @@ namespace osu.Game.Overlays.SearchableList protected abstract Color4 BackgroundColour { get; } protected abstract T DefaultTab { get; } + protected abstract U DefaultCategory { get; } protected virtual Drawable CreateSupplementaryControls() => null; /// @@ -109,6 +110,9 @@ namespace osu.Game.Overlays.SearchableList Tabs.Current.Value = DefaultTab; Tabs.Current.TriggerChange(); + + DisplayStyleControl.Dropdown.Current.Value = DefaultCategory; + DisplayStyleControl.Dropdown.Current.TriggerChange(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index 4e43caff23..5ccdc952ba 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Configuration; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -13,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - Children = new[] + Children = new Drawable[] { new SettingsCheckbox { @@ -25,6 +28,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio LabelText = "osu! music theme", Bindable = config.GetBindable(OsuSetting.MenuMusic) }, + new SettingsDropdown + { + LabelText = "Intro sequence", + Bindable = config.GetBindable(OsuSetting.IntroSequence), + Items = Enum.GetValues(typeof(IntroSequence)).Cast() + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index f063898a9f..7eec971b62 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -27,11 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections.Debug Bindable = frameworkConfig.GetBindable(FrameworkSetting.PerformanceLogging) }, new SettingsCheckbox - { - LabelText = "Bypass caching (slow)", - Bindable = config.GetBindable(DebugSetting.BypassCaching) - }, - new SettingsCheckbox { LabelText = "Bypass front-to-back render pass", Bindable = config.GetBindable(DebugSetting.BypassFrontToBackPass) diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs similarity index 63% rename from osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs rename to osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs index 6897b42f4f..db64c9a8ac 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/MemorySettings.cs @@ -1,26 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Platform; namespace osu.Game.Overlays.Settings.Sections.Debug { - public class GCSettings : SettingsSubsection + public class MemorySettings : SettingsSubsection { - protected override string Header => "Garbage Collector"; + protected override string Header => "Memory"; [BackgroundDependencyLoader] - private void load(FrameworkDebugConfigManager config) + private void load(FrameworkDebugConfigManager config, GameHost host) { Children = new Drawable[] { new SettingsButton { - Text = "Force garbage collection", - Action = GC.Collect + Text = "Clear all caches", + Action = host.Collect }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 0149cab802..f62de0b243 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections Children = new Drawable[] { new GeneralSettings(), - new GCSettings(), + new MemorySettings(), }; } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5d7542ca2b..35be930a2e 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -107,6 +107,8 @@ namespace osu.Game.Overlays.Settings.Sections private class SkinDropdownControl : DropdownControl { protected override string GenerateItemText(SkinInfo item) => item.ToString(); + + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index ae840c8c00..d48c0b6b66 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -46,8 +46,7 @@ namespace osu.Game.Overlays.Settings if (text == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - Add(text = new OsuSpriteText()); - FlowContent.SetLayoutPosition(text, -1); + FlowContent.Insert(-1, text = new OsuSpriteText()); } text.Text = value; diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index a80b7c1108..358f94b659 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -84,6 +84,7 @@ namespace osu.Game.Overlays.Settings Content.Anchor = Anchor.CentreLeft; Content.Origin = Anchor.CentreLeft; RelativeSizeAxes = Axes.Both; + ScrollbarVisible = false; } } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 474f529bb1..9dd0def453 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -186,7 +186,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); ContentContainer.Margin = new MarginPadding { Left = Sidebar?.DrawWidth ?? 0 }; - ContentContainer.Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; + Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; } protected class SettingsSectionsContainer : SectionsContainer diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 7f794e2927..5000156e97 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -57,7 +57,6 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Y = -15, Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronLeft diff --git a/osu.Game/Overlays/Social/FilterControl.cs b/osu.Game/Overlays/Social/FilterControl.cs index 6abf6ec98d..1c2cb95dfe 100644 --- a/osu.Game/Overlays/Social/FilterControl.cs +++ b/osu.Game/Overlays/Social/FilterControl.cs @@ -12,6 +12,7 @@ namespace osu.Game.Overlays.Social { protected override Color4 BackgroundColour => OsuColour.FromHex(@"47253a"); protected override SocialSortCriteria DefaultTab => SocialSortCriteria.Rank; + protected override SortDirection DefaultCategory => SortDirection.Ascending; public FilterControl() { diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 2b2b19b73a..d6b810366d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Toolbar { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(80).Opacity(180), - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0, }, Flow = new FillFlowContainer diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index f03df2ed93..b29aec5842 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader(true)] - private void load(MusicController music) + private void load(NowPlayingOverlay music) { StateContainer = music; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index b286cbfb1d..36387bb00d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Toolbar { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(150).Opacity(180), - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Depth = 2, Alpha = 0, }); diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index f4272ab15c..2c79f5bc0e 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -71,16 +71,11 @@ namespace osu.Game.Overlays.Toolbar // Scheduled to allow the flow layout to be computed before the line position is updated private void moveLineToCurrent() => ScheduleAfterChildren(() => { - foreach (var tabItem in TabContainer) + if (SelectedTab != null) { - if (tabItem.Value == Current.Value) - { - ModeButtonLine.MoveToX(tabItem.DrawPosition.X, !hasInitialPosition ? 0 : 200, Easing.OutQuint); - break; - } + ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 200, Easing.OutQuint); + hasInitialPosition = true; } - - hasInitialPosition = true; }); public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs index a4884dc2c1..6d876a77b1 100644 --- a/osu.Game/Overlays/Volume/MuteButton.cs +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -65,16 +65,15 @@ namespace osu.Game.Overlays.Volume { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(20), } }); - Current.ValueChanged += muted => + Current.BindValueChanged(muted => { icon.Icon = muted.NewValue ? FontAwesome.Solid.VolumeMute : FontAwesome.Solid.VolumeUp; - }; - - Current.TriggerChange(); + icon.Size = new Vector2(muted.NewValue ? 18 : 20); + icon.Margin = new MarginPadding { Right = muted.NewValue ? 2 : 0 }; + }, true); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 26235fa280..9cd3aac2cb 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -9,7 +9,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Volume { - public class VolumeControlReceptor : Container, IScrollBindingHandler, IHandleGlobalInput + public class VolumeControlReceptor : Container, IScrollBindingHandler, IHandleGlobalKeyboardInput { public Func ActionRequested; public Func ScrollActionRequested; diff --git a/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs b/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs index 2200caeb20..e85ebb5f3a 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -57,7 +58,12 @@ namespace osu.Game.Rulesets.Edit this.drawableRuleset = drawableRuleset; InternalChild = drawableRuleset; + } + [BackgroundDependencyLoader] + private void load() + { + drawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 2150726a42..61c2644c6f 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Judgements Font = OsuFont.Numeric.With(size: 12), Colour = judgementColour(Result.Type), Scale = new Vector2(0.85f, 1), - }, restrictSize: false) + }) }; } diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs index a634976b3c..cff669bf53 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs @@ -2,21 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mods { /// - /// Interface for a that applies changes to a - /// after conversion and post-processing has completed. + /// Interface for a that applies changes to a after conversion and post-processing has completed. /// - public interface IApplicableToBeatmap : IApplicableMod - where TObject : HitObject + public interface IApplicableToBeatmap : IApplicableMod { /// - /// Applies this to a . + /// Applies this to an . /// - /// The to apply to. - void ApplyToBeatmap(Beatmap beatmap); + /// The to apply to. + void ApplyToBeatmap(IBeatmap beatmap); } } diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 405d21c711..4f939362bb 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shaders; @@ -13,6 +14,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; +using osu.Game.Graphics.OpenGL.Vertices; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -153,9 +155,17 @@ namespace osu.Game.Rulesets.Mods private Vector2 flashlightSize; private float flashlightDim; + private readonly VertexBatch quadBatch = new QuadBatch(1, 1); + private readonly Action addAction; + public FlashlightDrawNode(Flashlight source) : base(source) { + addAction = v => quadBatch.Add(new PositionAndColourVertex + { + Position = v.Position, + Colour = v.Colour + }); } public override void ApplyState() @@ -179,10 +189,16 @@ namespace osu.Game.Rulesets.Mods shader.GetUniform("flashlightSize").UpdateValue(ref flashlightSize); shader.GetUniform("flashlightDim").UpdateValue(ref flashlightDim); - DrawQuad(Texture.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: vertexAction); + DrawQuad(Texture.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction); shader.Unbind(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + quadBatch?.Dispose(); + } } } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index a5f96087c0..9edf57ad00 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -13,27 +13,21 @@ using osuTK; namespace osu.Game.Rulesets.Mods { - public abstract class ModTimeRamp : Mod + public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap { - public override Type[] IncompatibleMods => new[] { typeof(ModTimeAdjust) }; - - protected abstract double FinalRateAdjustment { get; } - } - - public abstract class ModTimeRamp : ModTimeRamp, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap - where T : HitObject - { - private double finalRateTime; - - private double beginRampTime; - - private IAdjustableClock clock; - /// /// The point in the beatmap at which the final ramping rate should be reached. /// private const double final_rate_progress = 0.75f; + public override Type[] IncompatibleMods => new[] { typeof(ModTimeAdjust) }; + + protected abstract double FinalRateAdjustment { get; } + + private double finalRateTime; + private double beginRampTime; + private IAdjustableClock clock; + public virtual void ApplyToClock(IAdjustableClock clock) { this.clock = clock; @@ -44,7 +38,7 @@ namespace osu.Game.Rulesets.Mods applyAdjustment(1); } - public virtual void ApplyToBeatmap(Beatmap beatmap) + public virtual void ApplyToBeatmap(IBeatmap beatmap) { HitObject lastObject = beatmap.HitObjects.LastOrDefault(); diff --git a/osu.Game/Rulesets/Mods/ModType.cs b/osu.Game/Rulesets/Mods/ModType.cs index cd649728cf..e3c82e42f5 100644 --- a/osu.Game/Rulesets/Mods/ModType.cs +++ b/osu.Game/Rulesets/Mods/ModType.cs @@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Mods DifficultyIncrease, Conversion, Automation, - Fun + Fun, + System } } diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 5d71c8950b..b2e3abb59d 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -4,12 +4,10 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mods { - public class ModWindDown : ModTimeRamp - where T : HitObject + public class ModWindDown : ModTimeRamp { public override string Name => "Wind Down"; public override string Acronym => "WD"; @@ -19,6 +17,6 @@ namespace osu.Game.Rulesets.Mods protected override double FinalRateAdjustment => -0.25; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index aae85cec19..8df35a1de2 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -4,12 +4,10 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mods { - public class ModWindUp : ModTimeRamp - where T : HitObject + public class ModWindUp : ModTimeRamp { public override string Name => "Wind Up"; public override string Acronym => "WU"; @@ -19,6 +17,6 @@ namespace osu.Game.Rulesets.Mods protected override double FinalRateAdjustment => 0.5; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1f6ca4dd73..4a6f261905 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Game.Audio; -using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -19,14 +18,15 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables { - public abstract class DrawableHitObject : SkinReloadableDrawable, IHasAccentColour + [Cached(typeof(DrawableHitObject))] + public abstract class DrawableHitObject : SkinReloadableDrawable { public readonly HitObject HitObject; /// /// The colour used for various elements of this DrawableHitObject. /// - public virtual Color4 AccentColour { get; set; } = Color4.Gray; + public readonly Bindable AccentColour = new Bindable(Color4.Gray); // Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first protected virtual string SampleNamespace => null; @@ -83,7 +83,9 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart); - public readonly Bindable State = new Bindable(); + private readonly Bindable state = new Bindable(); + + public IBindable State => state; protected DrawableHitObject(HitObject hitObject) { @@ -118,33 +120,129 @@ namespace osu.Game.Rulesets.Objects.Drawables } } - protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}"); - protected override void LoadComplete() { base.LoadComplete(); - - State.ValueChanged += armed => - { - UpdateState(armed.NewValue); - - // apply any custom state overrides - ApplyCustomUpdateState?.Invoke(this, armed.NewValue); - - if (armed.NewValue == ArmedState.Hit) - PlaySamples(); - }; - - State.TriggerChange(); + updateState(ArmedState.Idle, true); } - protected abstract void UpdateState(ArmedState state); + #region State / Transform Management /// /// Bind to apply a custom state which can override the default implementation. /// public event Action ApplyCustomUpdateState; +#pragma warning disable 618 // (legacy state management) - can be removed 20200227 + + /// + /// Enables automatic transform management of this hitobject. Implementation of transforms should be done in and only. Rewinding and removing previous states is done automatically. + /// + /// + /// Going forward, this is the preferred way of implementing s. Previous functionality + /// is offered as a compatibility layer until all rulesets have been migrated across. + /// + [Obsolete("Use UpdateInitialTransforms()/UpdateStateTransforms() instead")] // can be removed 20200227 + protected virtual bool UseTransformStateManagement => true; + + protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}"); + + private void updateState(ArmedState newState, bool force = false) + { + if (State.Value == newState && !force) + return; + + if (UseTransformStateManagement) + { + double transformTime = HitObject.StartTime - InitialLifetimeOffset; + + base.ApplyTransformsAt(transformTime, true); + base.ClearTransformsAfter(transformTime, true); + + using (BeginAbsoluteSequence(transformTime, true)) + { + UpdateInitialTransforms(); + + var judgementOffset = Math.Min(HitObject.HitWindows?.HalfWindowFor(HitResult.Miss) ?? double.MaxValue, Result?.TimeOffset ?? 0); + + using (BeginDelayedSequence(InitialLifetimeOffset + judgementOffset, true)) + { + UpdateStateTransforms(newState); + state.Value = newState; + } + } + } + else + state.Value = newState; + + UpdateState(newState); + + // apply any custom state overrides + ApplyCustomUpdateState?.Invoke(this, newState); + + if (newState == ArmedState.Hit) + PlaySamples(); + } + + /// + /// Apply (generally fade-in) transforms leading into the start time. + /// The local drawable hierarchy is recursively delayed to for convenience. + /// + /// By default this will fade in the object from zero with no duration. + /// + /// + /// This is called once before every . This is to ensure a good state in the case + /// the was negative and potentially altered the pre-hit transforms. + /// + protected virtual void UpdateInitialTransforms() + { + this.FadeInFromZero(); + } + + /// + /// Apply transforms based on the current . Previous states are automatically cleared. + /// + /// The new armed state. + protected virtual void UpdateStateTransforms(ArmedState state) + { + } + + public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) + { + // When we are using automatic state management, parent calls to this should be blocked for safety. + if (!UseTransformStateManagement) + base.ClearTransformsAfter(time, propagateChildren, targetMember); + } + + public override void ApplyTransformsAt(double time, bool propagateChildren = false) + { + // When we are using automatic state management, parent calls to this should be blocked for safety. + if (!UseTransformStateManagement) + base.ApplyTransformsAt(time, propagateChildren); + } + + /// + /// Legacy method to handle state changes. + /// Should generally not be used when is true; use instead. + /// + /// The new armed state. + [Obsolete("Use UpdateInitialTransforms()/UpdateStateTransforms() instead")] // can be removed 20200227 + protected virtual void UpdateState(ArmedState state) + { + } + +#pragma warning restore 618 + + #endregion + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + if (HitObject is IHasComboInformation combo) + AccentColour.Value = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; + } + /// /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. @@ -165,7 +263,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Result.TimeOffset = 0; Result.Type = HitResult.None; - State.Value = ArmedState.Idle; + + updateState(ArmedState.Idle); } } } @@ -179,6 +278,40 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateResult(false); } + private double? lifetimeStart; + + public override double LifetimeStart + { + get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset); + set + { + base.LifetimeStart = value; + lifetimeStart = value; + } + } + + /// + /// A safe offset prior to the start time of at which this may begin displaying contents. + /// By default, s are assumed to display their contents within 10 seconds prior to the start time of . + /// + /// + /// This is only used as an optimisation to delay the initial update of this and may be tuned more aggressively if required. + /// It is indirectly used to decide the automatic transform offset provided to . + /// A more accurate should be set inside for an state. + /// + protected virtual double InitialLifetimeOffset => 10000; + + /// + /// Will be called at least once after this has become not alive. + /// + public virtual void OnKilled() + { + foreach (var nested in NestedHitObjects) + nested.OnKilled(); + + UpdateResult(false); + } + protected virtual void AddNested(DrawableHitObject h) { h.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r); @@ -212,27 +345,17 @@ namespace osu.Game.Rulesets.Objects.Drawables break; case HitResult.Miss: - State.Value = ArmedState.Miss; + updateState(ArmedState.Miss); break; default: - State.Value = ArmedState.Hit; + updateState(ArmedState.Hit); break; } OnNewResult?.Invoke(this, Result); } - /// - /// Will called at least once after the of this has been passed. - /// - internal void OnLifetimeEnd() - { - foreach (var nested in NestedHitObjects) - nested.OnLifetimeEnd(); - UpdateResult(false); - } - /// /// Processes this , checking if a scoring result has occurred. /// diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index 6e79d0b766..71e321f205 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -37,7 +37,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, PathType pathType, int repeatCount, List> nodeSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, + List> nodeSamples) { newCombo |= forceNewCombo; comboOffset += extraComboOffset; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f5b1cbcebf..e990938291 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Logging; using osu.Framework.MathUtils; namespace osu.Game.Rulesets.Objects.Legacy @@ -41,204 +40,192 @@ namespace osu.Game.Rulesets.Objects.Legacy [CanBeNull] public override HitObject Parse(string text) { - try + string[] split = text.Split(','); + + Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); + + double startTime = Parsing.ParseDouble(split[2]) + Offset; + + ConvertHitObjectType type = (ConvertHitObjectType)Parsing.ParseInt(split[3]); + + int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; + type &= ~ConvertHitObjectType.ComboOffset; + + bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); + type &= ~ConvertHitObjectType.NewCombo; + + var soundType = (LegacySoundType)Parsing.ParseInt(split[4]); + var bankInfo = new SampleBankInfo(); + + HitObject result = null; + + if (type.HasFlag(ConvertHitObjectType.Circle)) { - string[] split = text.Split(','); + result = CreateHit(pos, combo, comboOffset); - Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); + if (split.Length > 5) + readCustomSampleBanks(split[5], bankInfo); + } + else if (type.HasFlag(ConvertHitObjectType.Slider)) + { + PathType pathType = PathType.Catmull; + double? length = null; - double startTime = Parsing.ParseDouble(split[2]) + Offset; + string[] pointSplit = split[5].Split('|'); - ConvertHitObjectType type = (ConvertHitObjectType)Parsing.ParseInt(split[3]); + int pointCount = 1; + foreach (var t in pointSplit) + if (t.Length > 1) + pointCount++; - int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; - type &= ~ConvertHitObjectType.ComboOffset; + var points = new Vector2[pointCount]; - bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); - type &= ~ConvertHitObjectType.NewCombo; + int pointIndex = 1; - var soundType = (LegacySoundType)Parsing.ParseInt(split[4]); - var bankInfo = new SampleBankInfo(); - - HitObject result = null; - - if (type.HasFlag(ConvertHitObjectType.Circle)) + foreach (string t in pointSplit) { - result = CreateHit(pos, combo, comboOffset); - - if (split.Length > 5) - readCustomSampleBanks(split[5], bankInfo); - } - else if (type.HasFlag(ConvertHitObjectType.Slider)) - { - PathType pathType = PathType.Catmull; - double length = 0; - - string[] pointSplit = split[5].Split('|'); - - int pointCount = 1; - foreach (var t in pointSplit) - if (t.Length > 1) - pointCount++; - - var points = new Vector2[pointCount]; - - int pointIndex = 1; - - foreach (string t in pointSplit) + if (t.Length == 1) { - if (t.Length == 1) + switch (t) { - switch (t) - { - case @"C": - pathType = PathType.Catmull; - break; - - case @"B": - pathType = PathType.Bezier; - break; - - case @"L": - pathType = PathType.Linear; - break; - - case @"P": - pathType = PathType.PerfectCurve; - break; - } - - continue; - } - - string[] temp = t.Split(':'); - points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos; - } - - // osu-stable special-cased colinear perfect curves to a CurveType.Linear - bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); - - if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points)) - pathType = PathType.Linear; - - int repeatCount = Parsing.ParseInt(split[6]); - - if (repeatCount > 9000) - throw new ArgumentOutOfRangeException(nameof(repeatCount), @"Repeat count is way too high"); - - // osu-stable treated the first span of the slider as a repeat, but no repeats are happening - repeatCount = Math.Max(0, repeatCount - 1); - - if (split.Length > 7) - length = Math.Max(0, Parsing.ParseDouble(split[7])); - - if (split.Length > 10) - readCustomSampleBanks(split[10], bankInfo); - - // One node for each repeat + the start and end nodes - int nodes = repeatCount + 2; - - // Populate node sample bank infos with the default hit object sample bank - var nodeBankInfos = new List(); - for (int i = 0; i < nodes; i++) - nodeBankInfos.Add(bankInfo.Clone()); - - // Read any per-node sample banks - if (split.Length > 9 && split[9].Length > 0) - { - string[] sets = split[9].Split('|'); - - for (int i = 0; i < nodes; i++) - { - if (i >= sets.Length) + case @"C": + pathType = PathType.Catmull; break; - SampleBankInfo info = nodeBankInfos[i]; - readCustomSampleBanks(sets[i], info); - } - } - - // Populate node sound types with the default hit object sound type - var nodeSoundTypes = new List(); - for (int i = 0; i < nodes; i++) - nodeSoundTypes.Add(soundType); - - // Read any per-node sound types - if (split.Length > 8 && split[8].Length > 0) - { - string[] adds = split[8].Split('|'); - - for (int i = 0; i < nodes; i++) - { - if (i >= adds.Length) + case @"B": + pathType = PathType.Bezier; break; - int sound; - int.TryParse(adds[i], out sound); - nodeSoundTypes[i] = (LegacySoundType)sound; + case @"L": + pathType = PathType.Linear; + break; + + case @"P": + pathType = PathType.PerfectCurve; + break; } + + continue; } - // Generate the final per-node samples - var nodeSamples = new List>(nodes); + string[] temp = t.Split(':'); + points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos; + } + + // osu-stable special-cased colinear perfect curves to a CurveType.Linear + bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); + + if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points)) + pathType = PathType.Linear; + + int repeatCount = Parsing.ParseInt(split[6]); + + if (repeatCount > 9000) + throw new ArgumentOutOfRangeException(nameof(repeatCount), @"Repeat count is way too high"); + + // osu-stable treated the first span of the slider as a repeat, but no repeats are happening + repeatCount = Math.Max(0, repeatCount - 1); + + if (split.Length > 7) + { + length = Math.Max(0, Parsing.ParseDouble(split[7])); + if (length == 0) + length = null; + } + + if (split.Length > 10) + readCustomSampleBanks(split[10], bankInfo); + + // One node for each repeat + the start and end nodes + int nodes = repeatCount + 2; + + // Populate node sample bank infos with the default hit object sample bank + var nodeBankInfos = new List(); + for (int i = 0; i < nodes; i++) + nodeBankInfos.Add(bankInfo.Clone()); + + // Read any per-node sample banks + if (split.Length > 9 && split[9].Length > 0) + { + string[] sets = split[9].Split('|'); + for (int i = 0; i < nodes; i++) - nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - - result = CreateSlider(pos, combo, comboOffset, points, length, pathType, repeatCount, nodeSamples); - - // The samples are played when the slider ends, which is the last node - result.Samples = nodeSamples[nodeSamples.Count - 1]; - } - else if (type.HasFlag(ConvertHitObjectType.Spinner)) - { - double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); - - result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime); - - if (split.Length > 6) - readCustomSampleBanks(split[6], bankInfo); - } - else if (type.HasFlag(ConvertHitObjectType.Hold)) - { - // Note: Hold is generated by BMS converts - - double endTime = Math.Max(startTime, Parsing.ParseDouble(split[2])); - - if (split.Length > 5 && !string.IsNullOrEmpty(split[5])) { - string[] ss = split[5].Split(':'); - endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0])); - readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); + if (i >= sets.Length) + break; + + SampleBankInfo info = nodeBankInfos[i]; + readCustomSampleBanks(sets[i], info); } - - result = CreateHold(pos, combo, comboOffset, endTime + Offset); } - if (result == null) + // Populate node sound types with the default hit object sound type + var nodeSoundTypes = new List(); + for (int i = 0; i < nodes; i++) + nodeSoundTypes.Add(soundType); + + // Read any per-node sound types + if (split.Length > 8 && split[8].Length > 0) { - Logger.Log($"Unknown hit object type: {type}. Skipped.", level: LogLevel.Error); - return null; + string[] adds = split[8].Split('|'); + + for (int i = 0; i < nodes; i++) + { + if (i >= adds.Length) + break; + + int sound; + int.TryParse(adds[i], out sound); + nodeSoundTypes[i] = (LegacySoundType)sound; + } } - result.StartTime = startTime; + // Generate the final per-node samples + var nodeSamples = new List>(nodes); + for (int i = 0; i < nodes; i++) + nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - if (result.Samples.Count == 0) - result.Samples = convertSoundType(soundType, bankInfo); + result = CreateSlider(pos, combo, comboOffset, points, length, pathType, repeatCount, nodeSamples); - FirstObject = false; - - return result; + // The samples are played when the slider ends, which is the last node + result.Samples = nodeSamples[nodeSamples.Count - 1]; } - catch (FormatException) + else if (type.HasFlag(ConvertHitObjectType.Spinner)) { - Logger.Log("A hitobject could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); + double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); + + result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime); + + if (split.Length > 6) + readCustomSampleBanks(split[6], bankInfo); } - catch (OverflowException) + else if (type.HasFlag(ConvertHitObjectType.Hold)) { - Logger.Log("A hitobject could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important); + // Note: Hold is generated by BMS converts + + double endTime = Math.Max(startTime, Parsing.ParseDouble(split[2])); + + if (split.Length > 5 && !string.IsNullOrEmpty(split[5])) + { + string[] ss = split[5].Split(':'); + endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0])); + readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); + } + + result = CreateHold(pos, combo, comboOffset, endTime + Offset); } - return null; + if (result == null) + throw new InvalidDataException($"Unknown hit object type: {split[3]}"); + + result.StartTime = startTime; + + if (result.Samples.Count == 0) + result.Samples = convertSoundType(soundType, bankInfo); + + FirstObject = false; + + return result; } private void readCustomSampleBanks(string str, SampleBankInfo bankInfo) @@ -291,7 +278,8 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The slider repeat count. /// The samples to be played when the slider nodes are hit. This includes the head and tail of the slider. /// The hit object. - protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, PathType pathType, int repeatCount, List> nodeSamples); + protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, + List> nodeSamples); /// /// Creates a legacy Spinner-type hit object. diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs index c9f7224643..eab37b682c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy [Flags] internal enum ConvertHitObjectType { - Circle = 1 << 0, + Circle = 1, Slider = 1 << 1, NewCombo = 1 << 2, Spinner = 1 << 3, diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index b20a027e78..94aba95e90 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -26,7 +26,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, PathType pathType, int repeatCount, List> nodeSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, + List> nodeSamples) { return new ConvertSlider { diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 0a4e38df02..65102f1e89 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osuTK; using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; @@ -38,7 +37,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, PathType pathType, int repeatCount, List> nodeSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, + List> nodeSamples) { newCombo |= forceNewCombo; comboOffset += extraComboOffset; @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu Position = position, NewCombo = FirstObject || newCombo, ComboOffset = comboOffset, - Path = new SliderPath(pathType, controlPoints, Math.Max(0, length)), + Path = new SliderPath(pathType, controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount }; diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index 7c1514c1eb..eb598f1368 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko return new ConvertHit(); } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, PathType pathType, int repeatCount, List> nodeSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount, + List> nodeSamples) { return new ConvertSlider { diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 7cda7485f9..0d8796b4cb 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -3,13 +3,15 @@ using System; using System.Collections.Generic; +using System.Linq; using osuTK; namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { - public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset) + public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, + double? legacyLastTickOffset) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -36,24 +38,17 @@ namespace osu.Game.Rulesets.Objects var spanStartTime = startTime + span * spanDuration; var reversed = span % 2 == 1; - for (var d = tickDistance; d <= length; d += tickDistance) + var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd); + + if (reversed) { - if (d >= length - minDistanceFromEnd) - break; - - var pathProgress = d / length; - var timeProgress = reversed ? 1 - pathProgress : pathProgress; - - yield return new SliderEventDescriptor - { - Type = SliderEventType.Tick, - SpanIndex = span, - SpanStartTime = spanStartTime, - Time = spanStartTime + timeProgress * spanDuration, - PathProgress = pathProgress, - }; + // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets + ticks = ticks.Reverse(); } + foreach (var e in ticks) + yield return e; + if (span < spanCount - 1) { yield return new SliderEventDescriptor @@ -103,6 +98,40 @@ namespace osu.Game.Rulesets.Objects PathProgress = spanCount % 2, }; } + + /// + /// Generates the ticks for a span of the slider. + /// + /// The span index. + /// The start time of the span. + /// The duration of the span. + /// Whether the span is reversed. + /// The length of the path. + /// The distance between each tick. + /// The distance from the end of the path at which ticks are not allowed to be added. + /// A for each tick. If is true, the ticks will be returned in reverse-StartTime order. + private static IEnumerable generateTicks(int spanIndex, double spanStartTime, double spanDuration, bool reversed, double length, double tickDistance, + double minDistanceFromEnd) + { + for (var d = tickDistance; d <= length; d += tickDistance) + { + if (d >= length - minDistanceFromEnd) + break; + + // Always generate ticks from the start of the path rather than the span to ensure that ticks in repeat spans are positioned identically to those in non-repeat spans + var pathProgress = d / length; + var timeProgress = reversed ? 1 - pathProgress : pathProgress; + + yield return new SliderEventDescriptor + { + Type = SliderEventType.Tick, + SpanIndex = spanIndex, + SpanStartTime = spanStartTime, + Time = spanStartTime + timeProgress * spanDuration, + PathProgress = pathProgress, + }; + } + } } /// diff --git a/osu.Game/Rulesets/Replays/AutoGenerator.cs b/osu.Game/Rulesets/Replays/AutoGenerator.cs index 1d4cdbf04c..3319f30a6f 100644 --- a/osu.Game/Rulesets/Replays/AutoGenerator.cs +++ b/osu.Game/Rulesets/Replays/AutoGenerator.cs @@ -3,6 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Replays; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Replays { @@ -34,5 +35,13 @@ namespace osu.Game.Rulesets.Replays protected const double KEY_UP_DELAY = 50; #endregion + + protected virtual HitObject GetNextObject(int currentIndex) + { + if (currentIndex >= Beatmap.HitObjects.Count - 1) + return null; + + return Beatmap.HitObjects[currentIndex + 1]; + } } } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 3830fa5cbe..4c011388fa 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Replays /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. /// Disabling this can make replay playback smoother (useful for autoplay, currently). /// - public bool FrameAccuratePlayback = true; + public bool FrameAccuratePlayback = false; protected bool HasFrames => Frames.Count > 0; diff --git a/osu.Game/Rulesets/RulesetConfigCache.cs b/osu.Game/Rulesets/RulesetConfigCache.cs index 9a5a4d4acd..8c9e3c94e2 100644 --- a/osu.Game/Rulesets/RulesetConfigCache.cs +++ b/osu.Game/Rulesets/RulesetConfigCache.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets public IRulesetConfigManager GetConfigFor(Ruleset ruleset) { if (ruleset.RulesetInfo.ID == null) - throw new InvalidOperationException("The provided ruleset doesn't have a valid id."); + return null; return configCache.GetOrAdd(ruleset.RulesetInfo.ID.Value, _ => ruleset.CreateConfig(settingsStore)); } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index ca331ec339..d9cff86265 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; namespace osu.Game.Rulesets @@ -23,6 +24,21 @@ namespace osu.Game.Rulesets public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public override bool Equals(object obj) => obj is RulesetInfo rulesetInfo && Equals(rulesetInfo); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() + { + unchecked + { + var hashCode = ID.HasValue ? ID.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ (InstantiationInfo != null ? InstantiationInfo.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ Available.GetHashCode(); + return hashCode; + } + } + public override string ToString() => $"{Name} ({ShortName}) ID: {ID}"; } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index fd42f96c92..2d8c9f5b49 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets public RulesetStore(IDatabaseContextFactory factory) : base(factory) { - AddMissingRulesets(); + addMissingRulesets(); } /// @@ -52,13 +52,13 @@ namespace osu.Game.Rulesets /// /// All available rulesets. /// - public IEnumerable AvailableRulesets; + public IEnumerable AvailableRulesets { get; private set; } private static Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args) => loaded_assemblies.Keys.FirstOrDefault(a => a.FullName == args.Name); private const string ruleset_library_prefix = "osu.Game.Rulesets"; - protected void AddMissingRulesets() + private void addMissingRulesets() { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 47ce28db4c..e47df6b473 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -217,9 +217,7 @@ namespace osu.Game.Rulesets.Scoring private double baseScore; private double bonusScore; - protected ScoreProcessor() - { - } + private double scoreMultiplier = 1; public ScoreProcessor(DrawableRuleset drawableRuleset) { @@ -239,6 +237,15 @@ namespace osu.Game.Rulesets.Scoring } Mode.ValueChanged += _ => updateScore(); + Mods.ValueChanged += mods => + { + scoreMultiplier = 1; + + foreach (var m in mods.NewValue) + scoreMultiplier *= m.ScoreMultiplier; + + updateScore(); + }; } /// @@ -306,6 +313,9 @@ namespace osu.Game.Rulesets.Scoring /// /// Applies the score change of a to this . /// + /// + /// Any changes applied via this method can be reverted via . + /// /// The to apply. protected virtual void ApplyResult(JudgementResult result) { @@ -315,9 +325,6 @@ namespace osu.Game.Rulesets.Scoring JudgedHits++; - if (result.Type != HitResult.None) - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; - if (result.Judgement.AffectsCombo) { switch (result.Type) @@ -342,6 +349,9 @@ namespace osu.Game.Rulesets.Scoring } else { + if (result.HasResult) + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; + baseScore += result.Judgement.NumericResultFor(result); rollingMaxBaseScore += result.Judgement.MaxNumericResult; } @@ -350,7 +360,7 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Reverts the score change of a that was applied to this . + /// Reverts the score change of a that was applied to this via . /// /// The judgement scoring result. protected virtual void RevertResult(JudgementResult result) @@ -361,9 +371,6 @@ namespace osu.Game.Rulesets.Scoring JudgedHits--; - if (result.Type != HitResult.None) - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; - if (result.Judgement.IsBonus) { if (result.IsHit) @@ -371,6 +378,9 @@ namespace osu.Game.Rulesets.Scoring } else { + if (result.HasResult) + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; + baseScore -= result.Judgement.NumericResultFor(result); rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } @@ -397,11 +407,11 @@ namespace osu.Game.Rulesets.Scoring { default: case ScoringMode.Standardised: - return max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo.Value / maxHighestCombo) + bonusScore; + return (max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo.Value / maxHighestCombo) + bonusScore) * scoreMultiplier; case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) - return bonusScore + baseScore * (1 + Math.Max(0, HighestCombo.Value - 1) / 25); + return bonusScore + baseScore * ((1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier) / 25); } } diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs index 9bab065d1e..4b3c3f90f0 100644 --- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs +++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs @@ -20,7 +20,13 @@ namespace osu.Game.Rulesets.Timing /// /// The aggregate multiplier which this provides. /// - public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * 1000 / TimingPoint.BeatLength; + public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * BaseBeatLength / TimingPoint.BeatLength; + + /// + /// The base beat length to scale the provided multiplier relative to. + /// + /// For a of 1000, a with a beat length of 500 will increase the multiplier by 2. + public double BaseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; /// /// The velocity multiplier. diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 52fba9cab3..ccfd89adca 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -62,6 +62,22 @@ namespace osu.Game.Rulesets.UI public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; + private bool frameStablePlayback = true; + + /// + /// Whether to enable frame-stable playback. + /// + internal bool FrameStablePlayback + { + get => frameStablePlayback; + set + { + frameStablePlayback = false; + if (frameStabilityContainer != null) + frameStabilityContainer.FrameStablePlayback = value; + } + } + /// /// Invoked when a has been applied by a . /// @@ -109,8 +125,6 @@ namespace osu.Game.Rulesets.UI Beatmap = (Beatmap)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - applyBeatmapMods(mods); - KeyBindingInputManager = CreateInputManager(); playfield = new Lazy(CreatePlayfield); @@ -149,6 +163,7 @@ namespace osu.Game.Rulesets.UI { frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime) { + FrameStablePlayback = FrameStablePlayback, Child = KeyBindingInputManager .WithChild(CreatePlayfieldAdjustmentContainer() .WithChild(Playfield) @@ -269,19 +284,6 @@ namespace osu.Game.Rulesets.UI public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this); - /// - /// Applies the active mods to the Beatmap. - /// - /// - private void applyBeatmapMods(IReadOnlyList mods) - { - if (mods == null) - return; - - foreach (var mod in mods.OfType>()) - mod.ApplyToBeatmap(Beatmap); - } - /// /// Applies the active mods to this DrawableRuleset. /// diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 1cc56fff8b..05d3c02381 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.UI /// public int MaxCatchUpFrames { get; set; } = 5; + /// + /// Whether to enable frame-stable playback. + /// + internal bool FrameStablePlayback = true; + [Cached] public GameplayClock GameplayClock { get; } @@ -113,7 +118,13 @@ namespace osu.Game.Rulesets.UI try { - if (firstConsumption) + if (!FrameStablePlayback) + { + manualClock.CurrentTime = newProposedTime; + requireMoreUpdateLoops = false; + return; + } + else if (firstConsumption) { // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. // Instead we perform an initial seek to the proposed time. diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 2f3a384e95..9485189433 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -34,8 +34,14 @@ namespace osu.Game.Rulesets.UI protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) { - if (e.Kind == LifetimeBoundaryKind.End && e.Direction == LifetimeBoundaryCrossingDirection.Forward && e.Child is DrawableHitObject hitObject) - hitObject.OnLifetimeEnd(); + if (!(e.Child is DrawableHitObject hitObject)) + return; + + if ((e.Kind == LifetimeBoundaryKind.End && e.Direction == LifetimeBoundaryCrossingDirection.Forward) + || (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward)) + { + hitObject.OnKilled(); + } } } } diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 86feea09a8..88a2338b94 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -11,11 +11,14 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osuTK; +using osu.Framework.Bindables; namespace osu.Game.Rulesets.UI { public class ModIcon : Container, IHasTooltip { + public readonly BindableBool Highlighted = new BindableBool(); + private readonly SpriteIcon modIcon; private readonly SpriteIcon background; @@ -49,7 +52,6 @@ namespace osu.Game.Rulesets.UI Anchor = Anchor.Centre, Size = new Vector2(size), Icon = OsuIcon.ModBg, - Y = -6.5f, Shadow = true, }, modIcon = new SpriteIcon @@ -97,26 +99,12 @@ namespace osu.Game.Rulesets.UI highlightedColour = colours.PinkLight; break; } - - applyStyle(); } - private bool highlighted; - - public bool Highlighted + protected override void LoadComplete() { - get => highlighted; - - set - { - highlighted = value; - applyStyle(); - } - } - - private void applyStyle() - { - background.Colour = highlighted ? highlightedColour : backgroundColour; + base.LoadComplete(); + Highlighted.BindValueChanged(highlighted => background.Colour = highlighted.NewValue ? highlightedColour : backgroundColour, true); } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 42ec0b79b9..c1a4c9520e 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -69,6 +69,11 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected virtual bool UserScrollSpeedAdjustment => true; + /// + /// Whether beat lengths should scale relative to the most common beat length in the . + /// + protected virtual bool RelativeScaleBeatLengths => false; + /// /// Provides the default s that adjust the scrolling rate of s /// inside this . @@ -107,16 +112,38 @@ namespace osu.Game.Rulesets.UI.Scrolling [BackgroundDependencyLoader] private void load() { - // Calculate default multiplier control points + double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; + double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; + + if (RelativeScaleBeatLengths) + { + IReadOnlyList timingPoints = Beatmap.ControlPointInfo.TimingPoints; + double maxDuration = 0; + + for (int i = 0; i < timingPoints.Count; i++) + { + if (timingPoints[i].Time > lastObjectTime) + break; + + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastObjectTime; + double duration = endTime - timingPoints[i].Time; + + if (duration > maxDuration) + { + maxDuration = duration; + baseBeatLength = timingPoints[i].BeatLength; + } + } + } + + // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point var lastTimingPoint = new TimingControlPoint(); var lastDifficultyPoint = new DifficultyControlPoint(); - - // Merge timing + difficulty points var allPoints = new SortedList(Comparer.Default); allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints); - // Generate the timing points, making non-timing changes use the previous timing change + // Generate the timing points, making non-timing changes use the previous timing change and vice-versa var timingChanges = allPoints.Select(c => { var timingPoint = c as TimingControlPoint; @@ -131,14 +158,13 @@ namespace osu.Game.Rulesets.UI.Scrolling return new MultiplierControlPoint(c.Time) { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier, + BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, DifficultyPoint = lastDifficultyPoint }; }); - double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; - - // Perform some post processing of the timing changes + // Trim unwanted sequences of timing changes timingChanges = timingChanges // Collapse sections after the last hit object .Where(s => s.StartTime <= lastObjectTime) @@ -147,7 +173,6 @@ namespace osu.Game.Rulesets.UI.Scrolling controlPoints.AddRange(timingChanges); - // If we have no control points, add a default one if (controlPoints.Count == 0) controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 069e2d1a0b..1df8c8218f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -12,14 +12,19 @@ namespace osu.Game.Rulesets.UI.Scrolling { public class ScrollingHitObjectContainer : HitObjectContainer { - private readonly IBindable timeRange = new BindableDouble(); + /// + /// A multiplier applied to the length of the scrolling area to determine a safe default lifetime end for hitobjects. + /// This is only used to limit the lifetime end within reason, as proper lifetime management should be implemented on hitobjects themselves. + /// + private const float safe_lifetime_end_multiplier = 2; + private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } - private Cached initialStateCache = new Cached(); + private readonly Cached initialStateCache = new Cached(); public ScrollingHitObjectContainer() { @@ -88,24 +93,29 @@ namespace osu.Game.Rulesets.UI.Scrolling private void computeInitialStateRecursive(DrawableHitObject hitObject) { - hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value); + double endTime = hitObject.HitObject.StartTime; - if (hitObject.HitObject is IHasEndTime endTime) + if (hitObject.HitObject is IHasEndTime e) { + endTime = e.EndTime; + switch (direction.Value) { case ScrollingDirection.Up: case ScrollingDirection.Down: - hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, timeRange.Value, scrollLength); + hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength); break; case ScrollingDirection.Left: case ScrollingDirection.Right: - hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, timeRange.Value, scrollLength); + hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime, timeRange.Value, scrollLength); break; } } + hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value); + hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength); + foreach (var obj in hitObject.NestedHitObjects) { computeInitialStateRecursive(obj); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs index 0fdbd56c92..2e4b4b3a9a 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs @@ -80,6 +80,9 @@ namespace osu.Game.Scoring.Legacy else if (version >= 20121008) scoreInfo.OnlineScoreID = sr.ReadInt32(); + if (scoreInfo.OnlineScoreID <= 0) + scoreInfo.OnlineScoreID = null; + if (compressedReplay?.Length > 0) { using (var replayInStream = new MemoryStream(compressedReplay)) diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index bbe162cf7c..5dfaceccf5 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -11,8 +11,11 @@ namespace osu.Game.Screens { public abstract class BackgroundScreen : Screen, IEquatable { - protected BackgroundScreen() + private readonly bool animateOnEnter; + + protected BackgroundScreen(bool animateOnEnter = true) { + this.animateOnEnter = animateOnEnter; Anchor = Anchor.Centre; Origin = Anchor.Centre; } @@ -39,11 +42,14 @@ namespace osu.Game.Screens public override void OnEntering(IScreen last) { - this.FadeOut(); - this.MoveToX(x_movement_amount); + if (animateOnEnter) + { + this.FadeOut(); + this.MoveToX(x_movement_amount); - this.FadeIn(transition_length, Easing.InOutQuart); - this.MoveToX(0, transition_length, Easing.InOutQuart); + this.FadeIn(transition_length, Easing.InOutQuart); + this.MoveToX(0, transition_length, Easing.InOutQuart); + } base.OnEntering(last); } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index b6c2d016d2..5225740d0b 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -1,19 +1,28 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Screens.Play; +using osuTK; namespace osu.Game.Screens.Backgrounds { public class BackgroundScreenBeatmap : BackgroundScreen { + /// + /// The amount of blur to apply when full user blur is requested. + /// + public const float USER_BLUR_FACTOR = 25; + protected Background Background; private WorkingBeatmap beatmap; @@ -30,16 +39,17 @@ namespace osu.Game.Screens.Backgrounds /// public readonly Bindable BlurAmount = new Bindable(); - private readonly UserDimContainer fadeContainer; + private readonly DimmableBackground dimmable; - protected virtual UserDimContainer CreateFadeContainer() => new UserDimContainer { RelativeSizeAxes = Axes.Both }; + protected virtual DimmableBackground CreateFadeContainer() => new DimmableBackground { RelativeSizeAxes = Axes.Both }; public BackgroundScreenBeatmap(WorkingBeatmap beatmap = null) { Beatmap = beatmap; - InternalChild = fadeContainer = CreateFadeContainer(); - fadeContainer.EnableUserDim.BindTo(EnableUserDim); - fadeContainer.BlurAmount.BindTo(BlurAmount); + + InternalChild = dimmable = CreateFadeContainer(); + dimmable.EnableUserDim.BindTo(EnableUserDim); + dimmable.BlurAmount.BindTo(BlurAmount); } [BackgroundDependencyLoader] @@ -86,8 +96,8 @@ namespace osu.Game.Screens.Backgrounds } b.Depth = newDepth; - fadeContainer.Background = Background = b; - StoryboardReplacesBackground.BindTo(fadeContainer.StoryboardReplacesBackground); + dimmable.Background = Background = b; + StoryboardReplacesBackground.BindTo(dimmable.StoryboardReplacesBackground); } public override bool Equals(BackgroundScreen other) @@ -112,5 +122,70 @@ namespace osu.Game.Screens.Backgrounds Sprite.Texture = Beatmap?.Background ?? textures.Get(@"Backgrounds/bg1"); } } + + public class DimmableBackground : UserDimContainer + { + /// + /// The amount of blur to be applied to the background in addition to user-specified blur. + /// + /// + /// Used in contexts where there can potentially be both user and screen-specified blurring occuring at the same time, such as in + /// + public readonly Bindable BlurAmount = new Bindable(); + + public Background Background + { + get => background; + set + { + background?.Expire(); + + base.Add(background = value); + background.BlurTo(blurTarget, 0, Easing.OutQuint); + } + } + + private Bindable userBlurLevel { get; set; } + + private Background background; + + public override void Add(Drawable drawable) + { + if (drawable is Background) + throw new InvalidOperationException($"Use {nameof(Background)} to set a background."); + + base.Add(drawable); + } + + /// + /// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs. + /// + private Vector2 blurTarget => EnableUserDim.Value + ? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * USER_BLUR_FACTOR) + : new Vector2(BlurAmount.Value); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + userBlurLevel = config.GetBindable(OsuSetting.BlurLevel); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + userBlurLevel.ValueChanged += _ => UpdateVisuals(); + BlurAmount.ValueChanged += _ => UpdateVisuals(); + } + + protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value; // The background needs to be hidden in the case of it being replaced by the storyboard + + protected override void UpdateVisuals() + { + base.UpdateVisuals(); + + Background?.BlurTo(blurTarget, BACKGROUND_FADE_DURATION, Easing.OutQuint); + } + } } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 55338ea01a..2d7fe6a6a3 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Backgrounds private Bindable user; private Bindable skin; + public BackgroundScreenDefault(bool animateOnEnter = true) + : base(animateOnEnter) + { + } + [BackgroundDependencyLoader] private void load(IAPIProvider api, SkinManager skinManager) { diff --git a/osu.Game/Screens/Direct/OnlineListing.cs b/osu.Game/Screens/Direct/OnlineListing.cs deleted file mode 100644 index 8376383674..0000000000 --- a/osu.Game/Screens/Direct/OnlineListing.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Screens.Direct -{ - public class OnlineListing : ScreenWhiteBox - { - } -} diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs index 70c0cf623e..5854d66aa8 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons Scale = new Vector2(0.5f), X = 10, Masking = true, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Child = new Box { RelativeSizeAxes = Axes.Both } }; } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index c615656d60..0d16d8474b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -360,7 +360,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.2f), Color4.White), - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, }, new EquilateralTriangle { diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 8add730c4e..850349272e 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -9,6 +9,8 @@ using osu.Framework.Graphics.Shaders; using osu.Game.Screens.Menu; using osuTK; using osu.Framework.Screens; +using osu.Game.Configuration; +using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Screens { @@ -45,7 +47,27 @@ namespace osu.Game.Screens private OsuScreen loadableScreen; private ShaderPrecompiler precompiler; - protected virtual OsuScreen CreateLoadableScreen() => showDisclaimer ? (OsuScreen)new Disclaimer() : new Intro(); + private IntroSequence introSequence; + + protected virtual OsuScreen CreateLoadableScreen() + { + if (showDisclaimer) + return new Disclaimer(getIntroSequence()); + + return getIntroSequence(); + } + + private IntroScreen getIntroSequence() + { + switch (introSequence) + { + case IntroSequence.Circles: + return new IntroCircles(); + + default: + return new IntroTriangles(); + } + } protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler(); @@ -71,9 +93,10 @@ namespace osu.Game.Screens } [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load(OsuGameBase game, OsuConfigManager config) { showDisclaimer = game.IsDeployedBuild; + introSequence = config.Get(OsuSetting.IntroSequence); } /// diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index badd1e0549..1bf25a2504 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Menu { EdgeSmoothness = new Vector2(1.5f, 0), RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = Color4.White, Alpha = 0, }, diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 97231a1331..073ab639e3 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -21,7 +21,6 @@ namespace osu.Game.Screens.Menu { public class Disclaimer : StartupScreen { - private Intro intro; private SpriteIcon icon; private Color4 iconColour; private LinkFlowContainer textFlow; @@ -32,10 +31,13 @@ namespace osu.Game.Screens.Menu private const float icon_y = -85; private const float icon_size = 30; + private readonly OsuScreen nextScreen; + private readonly Bindable currentUser = new Bindable(); - public Disclaimer() + public Disclaimer(OsuScreen nextScreen = null) { + this.nextScreen = nextScreen; ValidForResume = false; } @@ -146,7 +148,8 @@ namespace osu.Game.Screens.Menu protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(intro = new Intro()); + if (nextScreen != null) + LoadComponentAsync(nextScreen); } public override void OnEntering(IScreen last) @@ -170,7 +173,7 @@ namespace osu.Game.Screens.Menu .Then(5500) .FadeOut(250) .ScaleTo(0.9f, 250, Easing.InQuint) - .Finally(d => this.Push(intro)); + .Finally(d => this.Push(nextScreen)); } } } diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/IntroCircles.cs similarity index 51% rename from osu.Game/Screens/Menu/Intro.cs rename to osu.Game/Screens/Menu/IntroCircles.cs index f6fbcf6498..4fa1a81123 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -12,41 +11,24 @@ using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; -using osu.Game.Screens.Backgrounds; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Menu { - public class Intro : StartupScreen + public class IntroCircles : IntroScreen { private const string menu_music_beatmap_hash = "3c8b1fcc9434dbb29e2fb613d3b9eada9d7bb6c125ceb32396c3b53437280c83"; - /// - /// Whether we have loaded the menu previously. - /// - public bool DidLoadMenu; - - private MainMenu mainMenu; private SampleChannel welcome; - private SampleChannel seeya; - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack(); - - private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); - - [Resolved] - private AudioManager audio { get; set; } - - private Bindable menuVoice; private Bindable menuMusic; + private Track track; + private WorkingBeatmap introBeatmap; [BackgroundDependencyLoader] - private void load(OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game) + private void load(OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game, ISampleStore samples) { - menuVoice = config.GetBindable(OsuSetting.MenuVoice); menuMusic = config.GetBindable(OsuSetting.MenuMusic); BeatmapSetInfo setInfo = null; @@ -75,15 +57,13 @@ namespace osu.Game.Screens.Menu introBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); track = introBeatmap.Track; - welcome = audio.Samples.Get(@"welcome"); - seeya = audio.Samples.Get(@"seeya"); + if (config.Get(OsuSetting.MenuVoice)) + welcome = samples.Get(@"welcome"); } private const double delay_step_one = 2300; private const double delay_step_two = 600; - public const int EXIT_DELAY = 3000; - protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -93,86 +73,34 @@ namespace osu.Game.Screens.Menu Beatmap.Value = introBeatmap; introBeatmap = null; - if (menuVoice.Value) - welcome.Play(); + welcome?.Play(); Scheduler.AddDelayed(delegate { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu. if (menuMusic.Value) { - track.Start(); + track.Restart(); track = null; } - LoadComponentAsync(mainMenu = new MainMenu()); + PrepareMenuLoad(); - Scheduler.AddDelayed(delegate - { - DidLoadMenu = true; - this.Push(mainMenu); - }, delay_step_one); + Scheduler.AddDelayed(LoadMenu, delay_step_one); }, delay_step_two); - } - logo.Colour = Color4.White; - logo.Ripple = false; - - const int quick_appear = 350; - - int initialMovementTime = logo.Alpha > 0.2f ? quick_appear : 0; - - logo.MoveTo(new Vector2(0.5f), initialMovementTime, Easing.OutQuint); - - if (!resuming) - { logo.ScaleTo(1); logo.FadeIn(); logo.PlayIntro(); } - else - { - logo.Triangles = false; - - logo - .ScaleTo(1, initialMovementTime, Easing.OutQuint) - .FadeIn(quick_appear, Easing.OutQuint) - .Then() - .RotateTo(20, EXIT_DELAY * 1.5f) - .FadeOut(EXIT_DELAY); - } } public override void OnSuspending(IScreen next) { + track = null; + this.FadeOut(300); base.OnSuspending(next); } - - public override bool OnExiting(IScreen next) - { - //cancel exiting if we haven't loaded the menu yet. - return !DidLoadMenu; - } - - public override void OnResuming(IScreen last) - { - this.FadeIn(300); - - double fadeOutTime = EXIT_DELAY; - //we also handle the exit transition. - if (menuVoice.Value) - seeya.Play(); - else - fadeOutTime = 500; - - audio.AddAdjustment(AdjustableProperty.Volume, exitingVolumeFade); - this.TransformBindableTo(exitingVolumeFade, 0, fadeOutTime).OnComplete(_ => this.Exit()); - - //don't want to fade out completely else we will stop running updates. - Game.FadeTo(0.01f, fadeOutTime); - - base.OnResuming(last); - } } } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs new file mode 100644 index 0000000000..4d0f7ff87a --- /dev/null +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Screens.Backgrounds; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public abstract class IntroScreen : StartupScreen + { + private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); + + public const int EXIT_DELAY = 3000; + + [Resolved] + private AudioManager audio { get; set; } + + private SampleChannel seeya; + + private Bindable menuVoice; + + private LeasedBindable beatmap; + + public new Bindable Beatmap => beatmap; + + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game) + { + // prevent user from changing beatmap while the intro is still runnning. + beatmap = base.Beatmap.BeginLease(false); + + menuVoice = config.GetBindable(OsuSetting.MenuVoice); + seeya = audio.Samples.Get(@"seeya"); + } + + /// + /// Whether we have loaded the menu previously. + /// + public bool DidLoadMenu { get; private set; } + + public override bool OnExiting(IScreen next) + { + //cancel exiting if we haven't loaded the menu yet. + return !DidLoadMenu; + } + + public override void OnResuming(IScreen last) + { + this.FadeIn(300); + + double fadeOutTime = EXIT_DELAY; + //we also handle the exit transition. + if (menuVoice.Value) + seeya.Play(); + else + fadeOutTime = 500; + + audio.AddAdjustment(AdjustableProperty.Volume, exitingVolumeFade); + this.TransformBindableTo(exitingVolumeFade, 0, fadeOutTime).OnComplete(_ => this.Exit()); + + //don't want to fade out completely else we will stop running updates. + Game.FadeTo(0.01f, fadeOutTime); + + base.OnResuming(last); + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + logo.Colour = Color4.White; + logo.Triangles = false; + logo.Ripple = false; + + if (!resuming) + { + logo.MoveTo(new Vector2(0.5f)); + logo.ScaleTo(Vector2.One); + logo.Hide(); + } + else + { + const int quick_appear = 350; + int initialMovementTime = logo.Alpha > 0.2f ? quick_appear : 0; + + logo.MoveTo(new Vector2(0.5f), initialMovementTime, Easing.OutQuint); + + logo + .ScaleTo(1, initialMovementTime, Easing.OutQuint) + .FadeIn(quick_appear, Easing.OutQuint) + .Then() + .RotateTo(20, EXIT_DELAY * 1.5f) + .FadeOut(EXIT_DELAY); + } + } + + private MainMenu mainMenu; + + protected void PrepareMenuLoad() + { + LoadComponentAsync(mainMenu = new MainMenu()); + } + + protected void LoadMenu() + { + beatmap.Return(); + + DidLoadMenu = true; + this.Push(mainMenu); + } + } +} diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs new file mode 100644 index 0000000000..db970dd76e --- /dev/null +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -0,0 +1,416 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.IO.Archives; +using osu.Game.Rulesets; +using osu.Game.Screens.Backgrounds; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public class IntroTriangles : IntroScreen + { + private const string menu_music_beatmap_hash = "a1556d0801b3a6b175dda32ef546f0ec812b400499f575c44fccbe9c67f9b1e5"; + + private SampleChannel welcome; + + protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false) + { + Alpha = 0, + }; + + [Resolved] + private AudioManager audio { get; set; } + + private Bindable menuMusic; + private Track track; + private WorkingBeatmap introBeatmap; + + private BackgroundScreenDefault background; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game) + { + menuMusic = config.GetBindable(OsuSetting.MenuMusic); + + BeatmapSetInfo setInfo = null; + + if (!menuMusic.Value) + { + var sets = beatmaps.GetAllUsableBeatmapSets(); + if (sets.Count > 0) + setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); + } + + if (setInfo == null) + { + setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == menu_music_beatmap_hash); + + if (setInfo == null) + { + // we need to import the default menu background beatmap + setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/triangles.osz"), "triangles.osz")).Result; + + setInfo.Protected = true; + beatmaps.Update(setInfo); + } + } + + introBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); + + track = introBeatmap.Track; + track.Reset(); + + if (config.Get(OsuSetting.MenuVoice) && !menuMusic.Value) + // triangles has welcome sound included in the track. only play this if the user doesn't want menu music. + welcome = audio.Samples.Get(@"welcome"); + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + logo.Triangles = true; + + if (!resuming) + { + Beatmap.Value = introBeatmap; + introBeatmap = null; + + PrepareMenuLoad(); + + LoadComponentAsync(new TrianglesIntroSequence(logo, background) + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedClock(menuMusic.Value ? track : null), + LoadMenu = LoadMenu + }, t => + { + AddInternal(t); + welcome?.Play(); + + // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. + if (menuMusic.Value) + track.Start(); + }); + } + } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + background.FadeOut(100); + } + + public override void OnSuspending(IScreen next) + { + track = null; + base.OnSuspending(next); + } + + private class TrianglesIntroSequence : CompositeDrawable + { + private readonly OsuLogo logo; + private readonly BackgroundScreenDefault background; + private OsuSpriteText welcomeText; + + private RulesetFlow rulesets; + private Container rulesetsScale; + private Container logoContainerSecondary; + private Drawable lazerLogo; + + private GlitchingTriangles triangles; + + public Action LoadMenu; + + public TrianglesIntroSequence(OsuLogo logo, BackgroundScreenDefault background) + { + this.logo = logo; + this.background = background; + } + + private OsuGameBase game; + + [BackgroundDependencyLoader] + private void load(TextureStore textures, OsuGameBase game) + { + this.game = game; + + InternalChildren = new Drawable[] + { + triangles = new GlitchingTriangles + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.4f, 0.16f) + }, + welcomeText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 10 }, + Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42), + Alpha = 1, + Spacing = new Vector2(5), + }, + rulesetsScale = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + rulesets = new RulesetFlow() + } + }, + logoContainerSecondary = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = lazerLogo = new LazerLogo(textures.GetStream("Menu/logo-triangles.mp4")) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + }; + } + + private const double text_1 = 200; + private const double text_2 = 400; + private const double text_3 = 700; + private const double text_4 = 900; + private const double text_glitch = 1060; + + private const double rulesets_1 = 1450; + private const double rulesets_2 = 1650; + private const double rulesets_3 = 1850; + + private const double logo_scale_duration = 920; + private const double logo_1 = 2080; + private const double logo_2 = logo_1 + logo_scale_duration; + + protected override void LoadComplete() + { + base.LoadComplete(); + + const float scale_start = 1.2f; + const float scale_adjust = 0.8f; + + rulesets.Hide(); + lazerLogo.Hide(); + background.Hide(); + + using (BeginAbsoluteSequence(0, true)) + { + using (BeginDelayedSequence(text_1, true)) + welcomeText.FadeIn().OnComplete(t => t.Text = "wel"); + + using (BeginDelayedSequence(text_2, true)) + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome"); + + using (BeginDelayedSequence(text_3, true)) + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to"); + + using (BeginDelayedSequence(text_4, true)) + { + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!"); + welcomeText.TransformTo(nameof(welcomeText.Spacing), new Vector2(50, 0), 5000); + } + + using (BeginDelayedSequence(text_glitch, true)) + triangles.FadeIn(); + + using (BeginDelayedSequence(rulesets_1, true)) + { + rulesetsScale.ScaleTo(0.8f, 1000); + rulesets.FadeIn().ScaleTo(1).TransformSpacingTo(new Vector2(200, 0)); + welcomeText.FadeOut(); + triangles.FadeOut(); + } + + using (BeginDelayedSequence(rulesets_2, true)) + { + rulesets.ScaleTo(2).TransformSpacingTo(new Vector2(30, 0)); + } + + using (BeginDelayedSequence(rulesets_3, true)) + { + rulesets.ScaleTo(4).TransformSpacingTo(new Vector2(10, 0)); + rulesetsScale.ScaleTo(1.3f, 1000); + } + + using (BeginDelayedSequence(logo_1, true)) + { + rulesets.FadeOut(); + + // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 + lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); + logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); + } + + using (BeginDelayedSequence(logo_2, true)) + { + lazerLogo.FadeOut().OnComplete(_ => + { + logoContainerSecondary.Remove(lazerLogo); + lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run. + + logo.FadeIn(); + background.FadeIn(); + + game.Add(new GameWideFlash()); + + LoadMenu(); + }); + } + } + } + + private class GameWideFlash : Box + { + private const double flash_length = 1000; + + public GameWideFlash() + { + Colour = Color4.White; + RelativeSizeAxes = Axes.Both; + Blending = BlendingParameters.Additive; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeOutFromOne(flash_length, Easing.Out); + } + } + + private class LazerLogo : CompositeDrawable + { + public LazerLogo(Stream videoStream) + { + Size = new Vector2(960); + + InternalChild = new VideoSprite(videoStream) + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 } + }; + } + } + + private class RulesetFlow : FillFlowContainer + { + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + var modes = new List(); + + foreach (var ruleset in rulesets.AvailableRulesets) + { + var icon = new ConstrainedIconContainer + { + Icon = ruleset.CreateInstance().CreateIcon(), + Size = new Vector2(30), + }; + + modes.Add(icon); + } + + AutoSizeAxes = Axes.Both; + Children = modes; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + } + + private class GlitchingTriangles : CompositeDrawable + { + public GlitchingTriangles() + { + RelativeSizeAxes = Axes.Both; + } + + private double? lastGenTime; + + private const double time_between_triangles = 22; + + protected override void Update() + { + base.Update(); + + if (lastGenTime == null || Time.Current - lastGenTime > time_between_triangles) + { + lastGenTime = (lastGenTime ?? Time.Current) + time_between_triangles; + + Drawable triangle = new OutlineTriangle(RNG.NextBool(), (RNG.NextSingle() + 0.2f) * 80) + { + RelativePositionAxes = Axes.Both, + Position = new Vector2(RNG.NextSingle(), RNG.NextSingle()), + }; + + AddInternal(triangle); + + triangle.FadeOutFromOne(120); + } + } + + /// + /// Represents a sprite that is drawn in a triangle shape, instead of a rectangle shape. + /// + public class OutlineTriangle : BufferedContainer + { + public OutlineTriangle(bool outlineOnly, float size) + { + Size = new Vector2(size); + + InternalChildren = new Drawable[] + { + new Triangle { RelativeSizeAxes = Axes.Both }, + }; + + if (outlineOnly) + { + AddInternal(new Triangle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Black, + Size = new Vector2(size - 5), + Blending = BlendingParameters.None, + }); + } + + Blending = BlendingParameters.Additive; + CacheDrawnFrameBuffer = true; + } + } + } + } + } +} diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 39bda799b5..9d0a5cd05b 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Menu public LogoVisualisation() { texture = Texture.WhitePixel; - Blending = BlendingMode.Additive; + Blending = BlendingParameters.Additive; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 5999cbdfb5..499b5089f6 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Charts; -using osu.Game.Screens.Direct; using osu.Game.Screens.Edit; using osu.Game.Screens.Multi; using osu.Game.Screens.Select; @@ -65,7 +64,6 @@ namespace osu.Game.Screens.Menu buttons = new ButtonSystem { OnChart = delegate { this.Push(new ChartListing()); }, - OnDirect = delegate { this.Push(new OnlineListing()); }, OnEdit = delegate { this.Push(new Editor()); }, OnSolo = onSolo, OnMulti = delegate { this.Push(new Multiplayer()); }, @@ -123,7 +121,7 @@ namespace osu.Game.Screens.Menu var track = Beatmap.Value.Track; var metadata = Beatmap.Value.Metadata; - if (last is Intro && track != null) + if (last is IntroScreen && track != null) { if (!track.IsRunning) { diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 95d0bf04b4..393964561c 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Menu // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, Alpha = 0, - Blending = BlendingMode.Additive + Blending = BlendingParameters.Additive }, rightBox = new Box { @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Menu Height = 1.5f, X = box_width, Alpha = 0, - Blending = BlendingMode.Additive + Blending = BlendingParameters.Additive } }; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 479b3d80b6..0c5bf12bdb 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Menu { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Alpha = 0 } } @@ -185,7 +185,7 @@ namespace osu.Game.Screens.Menu flashLayer = new Box { RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = Color4.White, Alpha = 0, }, diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 8e14f76e4b..d0d983bbff 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -14,6 +14,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { protected override Color4 BackgroundColour => OsuColour.FromHex(@"362e42"); protected override PrimaryFilter DefaultTab => PrimaryFilter.Open; + protected override SecondaryFilter DefaultCategory => SecondaryFilter.Public; protected override float ContentHorizontalPadding => base.ContentHorizontalPadding + OsuScreen.HORIZONTAL_OVERFLOW_PADDING; diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs index 73994fa369..a52d43acf4 100644 --- a/osu.Game/Screens/Multi/Match/Components/Header.cs +++ b/osu.Game/Screens/Multi/Match/Components/Header.cs @@ -25,10 +25,14 @@ namespace osu.Game.Screens.Multi.Match.Components { public const float HEIGHT = 200; - public MatchTabControl Tabs; + public readonly BindableBool ShowBeatmapPanel = new BindableBool(); + + public MatchTabControl Tabs { get; private set; } public Action RequestBeatmapSelection; + private MatchBeatmapPanel beatmapPanel; + public Header() { RelativeSizeAxes = Axes.X; @@ -53,8 +57,14 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.4f), Color4.Black.Opacity(0.6f)), + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.7f), Color4.Black.Opacity(0.8f)), }, + beatmapPanel = new MatchBeatmapPanel + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 100 }, + } } }, new Box @@ -114,6 +124,12 @@ namespace osu.Game.Screens.Multi.Match.Components beatmapButton.Action = () => RequestBeatmapSelection?.Invoke(); } + protected override void LoadComplete() + { + base.LoadComplete(); + ShowBeatmapPanel.BindValueChanged(value => beatmapPanel.FadeTo(value.NewValue ? 1 : 0, 200, Easing.OutQuint), true); + } + private class BeatmapSelectButton : HeaderButton { [Resolved(typeof(Room), nameof(Room.RoomID))] diff --git a/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs b/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs index f3412d0be7..de6ece6a05 100644 --- a/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/HeaderButton.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Multi.Match.Components { RelativeSizeAxes = Axes.Both, Alpha = 0.15f, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, }, }); } diff --git a/osu.Game/Screens/Multi/Match/Components/Info.cs b/osu.Game/Screens/Multi/Match/Components/Info.cs index a185c4db50..74f000c21f 100644 --- a/osu.Game/Screens/Multi/Match/Components/Info.cs +++ b/osu.Game/Screens/Multi/Match/Components/Info.cs @@ -28,7 +28,6 @@ namespace osu.Game.Screens.Multi.Match.Components private void load() { ReadyButton readyButton; - ViewBeatmapButton viewBeatmapButton; HostInfo hostInfo; InternalChildren = new Drawable[] @@ -80,7 +79,6 @@ namespace osu.Game.Screens.Multi.Match.Components Direction = FillDirection.Horizontal, Children = new Drawable[] { - viewBeatmapButton = new ViewBeatmapButton(), readyButton = new ReadyButton { Action = () => OnStart?.Invoke() @@ -91,11 +89,7 @@ namespace osu.Game.Screens.Multi.Match.Components }, }; - CurrentItem.BindValueChanged(item => - { - viewBeatmapButton.Beatmap.Value = item.NewValue?.Beatmap; - readyButton.Beatmap.Value = item.NewValue?.Beatmap; - }, true); + CurrentItem.BindValueChanged(item => readyButton.Beatmap.Value = item.NewValue?.Beatmap, true); hostInfo.Host.BindTo(Host); } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs b/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs new file mode 100644 index 0000000000..7c1fe91393 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/MatchBeatmapPanel.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class MatchBeatmapPanel : MultiplayerComposite + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private CancellationTokenSource loadCancellation; + private GetBeatmapSetRequest request; + private DirectGridPanel panel; + + public MatchBeatmapPanel() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + CurrentItem.BindValueChanged(item => loadNewPanel(item.NewValue?.Beatmap), true); + } + + private void loadNewPanel(BeatmapInfo beatmap) + { + loadCancellation?.Cancel(); + request?.Cancel(); + + panel?.FadeOut(200); + panel?.Expire(); + panel = null; + + if (beatmap?.OnlineBeatmapID == null) + return; + + loadCancellation = new CancellationTokenSource(); + + request = new GetBeatmapSetRequest(beatmap.OnlineBeatmapID.Value, BeatmapSetLookupType.BeatmapId); + request.Success += res => Schedule(() => + { + panel = new DirectGridPanel(res.ToBeatmapSet(rulesets)); + LoadComponentAsync(panel, AddInternal, loadCancellation.Token); + }); + + api.Queue(request); + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index fff713f026..ae27e53813 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -33,6 +33,8 @@ namespace osu.Game.Screens.Multi.Match.Components }, true); } + protected override bool IsOnlineScope => true; + protected override APIRequest FetchScores(Action> scoresCallback) { if (roomId.Value == null) diff --git a/osu.Game/Screens/Multi/Match/Components/ViewBeatmapButton.cs b/osu.Game/Screens/Multi/Match/Components/ViewBeatmapButton.cs deleted file mode 100644 index 8d1ff21124..0000000000 --- a/osu.Game/Screens/Multi/Match/Components/ViewBeatmapButton.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osuTK; - -namespace osu.Game.Screens.Multi.Match.Components -{ - public class ViewBeatmapButton : HeaderButton - { - public readonly Bindable Beatmap = new Bindable(); - - [Resolved(CanBeNull = true)] - private OsuGame osuGame { get; set; } - - public ViewBeatmapButton() - { - RelativeSizeAxes = Axes.Y; - Size = new Vector2(200, 1); - - Text = "View beatmap"; - } - - [BackgroundDependencyLoader] - private void load() - { - if (osuGame != null) - Beatmap.BindValueChanged(beatmap => updateAction(beatmap.NewValue), true); - } - - private void updateAction(BeatmapInfo beatmap) - { - if (beatmap == null) - { - Enabled.Value = false; - return; - } - - Action = () => osuGame.ShowBeatmap(beatmap.OnlineBeatmapID ?? 0); - Enabled.Value = true; - } - } -} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 5469d4c8c8..f3e10db444 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.GameTypes; @@ -18,7 +19,8 @@ using PlaylistItem = osu.Game.Online.Multiplayer.PlaylistItem; namespace osu.Game.Screens.Multi.Match { - public class MatchSubScreen : MultiplayerSubScreen + [Cached(typeof(IPreviewTrackOwner))] + public class MatchSubScreen : MultiplayerSubScreen, IPreviewTrackOwner { public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -44,6 +46,9 @@ namespace osu.Game.Screens.Multi.Match [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } + [Resolved(CanBeNull = true)] private OsuGame game { get; set; } @@ -146,18 +151,12 @@ namespace osu.Game.Screens.Multi.Match { const float fade_duration = 500; - if (tab.NewValue is SettingsMatchPage) - { - settings.Show(); - info.FadeOut(fade_duration, Easing.OutQuint); - bottomRow.FadeOut(fade_duration, Easing.OutQuint); - } - else - { - settings.Hide(); - info.FadeIn(fade_duration, Easing.OutQuint); - bottomRow.FadeIn(fade_duration, Easing.OutQuint); - } + var settingsDisplayed = tab.NewValue is SettingsMatchPage; + + header.ShowBeatmapPanel.Value = !settingsDisplayed; + settings.State.Value = settingsDisplayed ? Visibility.Visible : Visibility.Hidden; + info.FadeTo(settingsDisplayed ? 0 : 1, fade_duration, Easing.OutQuint); + bottomRow.FadeTo(settingsDisplayed ? 0 : 1, fade_duration, Easing.OutQuint); }, true); chat.Exit += () => @@ -179,8 +178,8 @@ namespace osu.Game.Screens.Multi.Match public override bool OnExiting(IScreen next) { RoomManager?.PartRoom(); - Mods.Value = Array.Empty(); + previewTrackManager.StopAnyPlaying(this); return base.OnExiting(next); } @@ -198,6 +197,8 @@ namespace osu.Game.Screens.Multi.Match if (e.NewValue?.Ruleset != null) Ruleset.Value = e.NewValue.Ruleset; + + previewTrackManager.StopAnyPlaying(this); } /// @@ -223,6 +224,8 @@ namespace osu.Game.Screens.Multi.Match private void onStart() { + previewTrackManager.StopAnyPlaying(this); + switch (type.Value) { default: diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 9ffd620e55..90806bab6e 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Multi public override bool OnExiting(IScreen next) { - if (!(screenStack.CurrentScreen is LoungeSubScreen)) + if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) { screenStack.Exit(); return true; diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index d390787090..6fdee85f45 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,26 +16,45 @@ namespace osu.Game.Screens.Play { public class BreakOverlay : Container { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + /// + /// The duration of the break overlay fading. + /// + public const double BREAK_FADE_DURATION = BreakPeriod.MIN_BREAK_DURATION / 2; + private const float remaining_time_container_max_size = 0.3f; private const int vertical_margin = 25; - private List breaks; - private readonly Container fadeContainer; - public List Breaks + private IReadOnlyList breaks; + + public IReadOnlyList Breaks { get => breaks; set { breaks = value; - initializeBreaks(); + + // reset index in case the new breaks list is smaller than last one + isBreakTime.Value = false; + CurrentBreakIndex = 0; + + if (IsLoaded) + initializeBreaks(); } } public override bool RemoveCompletedTransforms => false; + /// + /// Whether the gameplay is currently in a break. + /// + public IBindable IsBreakTime => isBreakTime; + + protected int CurrentBreakIndex; + + private readonly BindableBool isBreakTime = new BindableBool(); + private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; @@ -109,10 +129,36 @@ namespace osu.Game.Screens.Play initializeBreaks(); } + protected override void Update() + { + base.Update(); + updateBreakTimeBindable(); + } + + private void updateBreakTimeBindable() + { + if (breaks == null || breaks.Count == 0) + return; + + var time = Clock.CurrentTime; + + if (time > breaks[CurrentBreakIndex].EndTime) + { + while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) + CurrentBreakIndex++; + } + else + { + while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) + CurrentBreakIndex--; + } + + var currentBreak = breaks[CurrentBreakIndex]; + isBreakTime.Value = currentBreak.HasEffect && currentBreak.Contains(time); + } + private void initializeBreaks() { - if (!IsLoaded) return; // we need a clock. - FinishTransforms(true); Scheduler.CancelDelayedTasks(); @@ -125,25 +171,25 @@ namespace osu.Game.Screens.Play using (BeginAbsoluteSequence(b.StartTime, true)) { - fadeContainer.FadeIn(fade_duration); - breakArrows.Show(fade_duration); + fadeContainer.FadeIn(BREAK_FADE_DURATION); + breakArrows.Show(BREAK_FADE_DURATION); remainingTimeAdjustmentBox - .ResizeWidthTo(remaining_time_container_max_size, fade_duration, Easing.OutQuint) - .Delay(b.Duration - fade_duration) + .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) + .Delay(b.Duration - BREAK_FADE_DURATION) .ResizeWidthTo(0); remainingTimeBox - .ResizeWidthTo(0, b.Duration - fade_duration) + .ResizeWidthTo(0, b.Duration - BREAK_FADE_DURATION) .Then() .ResizeWidthTo(1); remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); - using (BeginDelayedSequence(b.Duration - fade_duration, true)) + using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION, true)) { - fadeContainer.FadeOut(fade_duration); - breakArrows.Hide(fade_duration); + fadeContainer.FadeOut(BREAK_FADE_DURATION); + breakArrows.Hide(BREAK_FADE_DURATION); } } } diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs new file mode 100644 index 0000000000..2154526e54 --- /dev/null +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Graphics.Containers; +using osu.Game.Storyboards; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Screens.Play +{ + /// + /// A container that handles loading, as well as applies user-specified visual settings to it. + /// + public class DimmableStoryboard : UserDimContainer + { + private readonly Storyboard storyboard; + private DrawableStoryboard drawableStoryboard; + + public DimmableStoryboard(Storyboard storyboard) + { + this.storyboard = storyboard; + } + + [BackgroundDependencyLoader] + private void load() + { + initializeStoryboard(false); + } + + protected override void LoadComplete() + { + ShowStoryboard.BindValueChanged(_ => initializeStoryboard(true), true); + base.LoadComplete(); + } + + protected override bool ShowDimContent => ShowStoryboard.Value && DimLevel < 1; + + private void initializeStoryboard(bool async) + { + if (drawableStoryboard != null) + return; + + if (!ShowStoryboard.Value) + return; + + drawableStoryboard = storyboard.CreateDrawable(); + drawableStoryboard.Masking = true; + + if (async) + LoadComponentAsync(drawableStoryboard, Add); + else + Add(drawableStoryboard); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0da9c77f25..e7398be176 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -26,7 +26,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Skinning; -using osu.Game.Storyboards.Drawables; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -67,6 +66,8 @@ namespace osu.Game.Screens.Play private SampleChannel sampleRestart; + private BreakOverlay breakOverlay; + protected ScoreProcessor ScoreProcessor { get; private set; } protected DrawableRuleset DrawableRuleset { get; private set; } @@ -76,6 +77,8 @@ namespace osu.Game.Screens.Play protected GameplayClockContainer GameplayClockContainer { get; private set; } + protected DimmableStoryboard DimmableStoryboard { get; private set; } + [Cached] [Cached(Type = typeof(IBindable>))] protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); @@ -109,7 +112,6 @@ namespace osu.Game.Screens.Play sampleRestart = audio.Samples.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); ScoreProcessor = DrawableRuleset.CreateScoreProcessor(); ScoreProcessor.Mods.BindTo(Mods); @@ -121,7 +123,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Children = new[] { - StoryboardContainer = CreateStoryboardContainer(), + DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }, new ScalingContainer(ScalingMode.Gameplay) { Child = new LocalSkinOverrideContainer(working.Skin) @@ -134,7 +136,7 @@ namespace osu.Game.Screens.Play } } }, - new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -199,9 +201,6 @@ namespace osu.Game.Screens.Play // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - // load storyboard as part of player's load if we can - initializeStoryboard(false); - // Bind ScoreProcessor to ourselves ScoreProcessor.AllJudged += onCompletion; ScoreProcessor.Failed += onFail; @@ -258,7 +257,7 @@ namespace osu.Game.Screens.Play private void performImmediateExit() { // if a restart has been requested, cancel any pending completion (user has shown intent to restart). - onCompletionEvent = null; + completionProgressDelegate?.Cancel(); ValidForResume = false; @@ -278,20 +277,16 @@ namespace osu.Game.Screens.Play sampleRestart?.Play(); - // if a restart has been requested, cancel any pending completion (user has shown intent to restart). - onCompletionEvent = null; - - ValidForResume = false; RestartRequested?.Invoke(); - this.Exit(); + performImmediateExit(); } - private ScheduledDelegate onCompletionEvent; + private ScheduledDelegate completionProgressDelegate; private void onCompletion() { // Only show the completion screen if the player hasn't failed - if (ScoreProcessor.HasFailed || onCompletionEvent != null) + if (ScoreProcessor.HasFailed || completionProgressDelegate != null) return; ValidForResume = false; @@ -300,7 +295,7 @@ namespace osu.Game.Screens.Play using (BeginDelayedSequence(1000)) { - onCompletionEvent = Schedule(delegate + completionProgressDelegate = Schedule(delegate { if (!this.IsCurrentScreen()) return; @@ -309,8 +304,6 @@ namespace osu.Game.Screens.Play scoreManager.Import(score).Wait(); this.Push(CreateResults(score)); - - onCompletionEvent = null; }); } } @@ -334,41 +327,6 @@ namespace osu.Game.Screens.Play protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); - #region Storyboard - - private DrawableStoryboard storyboard; - protected UserDimContainer StoryboardContainer { get; private set; } - - protected virtual UserDimContainer CreateStoryboardContainer() => new UserDimContainer(true) - { - RelativeSizeAxes = Axes.Both, - Alpha = 1, - EnableUserDim = { Value = true } - }; - - private Bindable showStoryboard; - - private void initializeStoryboard(bool asyncLoad) - { - if (StoryboardContainer == null || storyboard != null) - return; - - if (!showStoryboard.Value) - return; - - var beatmap = Beatmap.Value; - - storyboard = beatmap.Storyboard.CreateDrawable(); - storyboard.Masking = true; - - if (asyncLoad) - LoadComponentAsync(storyboard, StoryboardContainer.Add); - else - StoryboardContainer.Add(storyboard); - } - - #endregion - #region Fail Logic protected FailOverlay FailOverlay { get; private set; } @@ -455,8 +413,7 @@ namespace osu.Game.Screens.Play PauseOverlay.Hide(); // breaks and time-based conditions may allow instant resume. - double time = GameplayClockContainer.GameplayClock.CurrentTime; - if (Beatmap.Value.Beatmap.Breaks.Any(b => b.Contains(time)) || time < Beatmap.Value.Beatmap.HitObjects.First().StartTime) + if (breakOverlay.IsBreakTime.Value || GameplayClockContainer.GameplayClock.CurrentTime < Beatmap.Value.Beatmap.HitObjects.First().StartTime) completeResume(); else DrawableRuleset.RequestResume(completeResume); @@ -486,13 +443,11 @@ namespace osu.Game.Screens.Play .Delay(250) .FadeIn(250); - showStoryboard.ValueChanged += _ => initializeStoryboard(true); - Background.EnableUserDim.Value = true; Background.BlurAmount.Value = 0; Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); - StoryboardContainer.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); + DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; @@ -511,10 +466,10 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { - if (onCompletionEvent != null) + if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) { - // Proceed to result screen if beatmap already finished playing - onCompletionEvent.RunTask(); + // proceed to result screen if beatmap already finished playing + completionProgressDelegate.RunTask(); return true; } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 38dd179f25..d6c2b59d98 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -161,6 +161,8 @@ namespace osu.Game.Screens.Play private Visibility state; private ScheduledDelegate scheduledHide; + public override bool IsPresent => true; + public Visibility State { get => state; @@ -201,14 +203,15 @@ namespace osu.Game.Screens.Play protected override bool OnMouseDown(MouseDownEvent e) { + Show(); scheduledHide?.Cancel(); - return base.OnMouseDown(e); + return true; } protected override bool OnMouseUp(MouseUpEvent e) { Show(); - return base.OnMouseUp(e); + return true; } public override void Hide() => State = Visibility.Hidden; diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index 5b7a9574b6..9c56725c4e 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Play return base.Invalidate(invalidation, source, shallPropagate); } - private Cached layout = new Cached(); + private readonly Cached layout = new Cached(); private ScheduledDelegate scheduledCreate; protected override void Update() diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs index f53fb75e1a..cac26b3dbf 100644 --- a/osu.Game/Screens/Ranking/Results.cs +++ b/osu.Game/Screens/Ranking/Results.cs @@ -256,9 +256,12 @@ namespace osu.Game.Screens.Ranking } }; - foreach (var t in CreateResultPages()) - modeChangeButtons.AddItem(t); - modeChangeButtons.Current.Value = modeChangeButtons.Items.FirstOrDefault(); + var pages = CreateResultPages(); + + foreach (var p in pages) + modeChangeButtons.AddItem(p); + + modeChangeButtons.Current.Value = pages.FirstOrDefault(); modeChangeButtons.Current.BindValueChanged(page => { diff --git a/osu.Game/Screens/ScreenWhiteBox.cs b/osu.Game/Screens/ScreenWhiteBox.cs index 5648dd997b..6c5854d17e 100644 --- a/osu.Game/Screens/ScreenWhiteBox.cs +++ b/osu.Game/Screens/ScreenWhiteBox.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens Colour = getColourFor(GetType()), Alpha = 0.2f, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, }, textContainer = new FillFlowContainer { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 16354534f4..23c581c6f9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -24,8 +24,11 @@ using osu.Game.Screens.Select.Carousel; namespace osu.Game.Screens.Select { - public class BeatmapCarousel : OsuScrollContainer + public class BeatmapCarousel : CompositeDrawable { + private const float bleed_top = FilterControl.HEIGHT; + private const float bleed_bottom = Footer.HEIGHT; + /// /// Triggered when the loaded change and are completely loaded. /// @@ -58,6 +61,8 @@ namespace osu.Game.Screens.Select /// public bool BeatmapSetsLoaded { get; private set; } + private readonly OsuScrollContainer scroll; + private IEnumerable beatmapSets => root.Children.OfType(); public IEnumerable BeatmapSets @@ -81,7 +86,8 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); scrollPositionCache.Invalidate(); - Schedule(() => + // Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run. + SchedulerAfterChildren.Add(() => { BeatmapSetsChanged?.Invoke(); BeatmapSetsLoaded = true; @@ -89,8 +95,8 @@ namespace osu.Game.Screens.Select } private readonly List yPositions = new List(); - private Cached itemsCache = new Cached(); - private Cached scrollPositionCache = new Cached(); + private readonly Cached itemsCache = new Cached(); + private readonly Cached scrollPositionCache = new Cached(); private readonly Container scrollableContent; @@ -106,13 +112,17 @@ namespace osu.Game.Screens.Select public BeatmapCarousel() { root = new CarouselRoot(this); - Child = new OsuContextMenuContainer + InternalChild = new OsuContextMenuContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = scrollableContent = new Container + RelativeSizeAxes = Axes.Both, + Child = scroll = new CarouselScrollContainer { - RelativeSizeAxes = Axes.X, + Masking = false, + RelativeSizeAxes = Axes.Both, + Child = scrollableContent = new Container + { + RelativeSizeAxes = Axes.X, + } } }; } @@ -123,25 +133,22 @@ namespace osu.Game.Screens.Select config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); - RightClickScrollingEnabled.ValueChanged += enabled => RightMouseScrollbar = enabled.NewValue; + RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); loadBeatmapSets(beatmaps.GetAllUsableBeatmapSetsEnumerable()); } - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - Schedule(() => - { - var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); + var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); - if (existingSet == null) - return; + if (existingSet == null) + return; - root.RemoveChild(existingSet); - itemsCache.Invalidate(); - }); - } + root.RemoveChild(existingSet); + itemsCache.Invalidate(); + }); public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { @@ -338,6 +345,25 @@ namespace osu.Game.Screens.Select public bool AllowSelection = true; + /// + /// Half the height of the visible content. + /// + /// This is different from the height of , since + /// the beatmap carousel bleeds into the and the + /// + /// + private float visibleHalfHeight => (DrawHeight + bleed_bottom + bleed_top) / 2; + + /// + /// The position of the lower visible bound with respect to the current scroll position. + /// + private float visibleBottomBound => scroll.Current + DrawHeight + bleed_bottom; + + /// + /// The position of the upper visible bound with respect to the current scroll position. + /// + private float visibleUpperBound => scroll.Current - bleed_top; + public void FlushPendingFilterOperations() { if (PendingFilter?.Completed == false) @@ -414,6 +440,8 @@ namespace osu.Game.Screens.Select return true; } + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos); + protected override void Update() { base.Update(); @@ -424,17 +452,15 @@ namespace osu.Game.Screens.Select if (!scrollPositionCache.IsValid) updateScrollPosition(); - float drawHeight = DrawHeight; - // Remove all items that should no longer be on-screen - scrollableContent.RemoveAll(p => p.Y < Current - p.DrawHeight || p.Y > Current + drawHeight || !p.IsPresent); + scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); // Find index range of all items that should be on-screen Trace.Assert(Items.Count == yPositions.Count); - int firstIndex = yPositions.BinarySearch(Current - DrawableCarouselItem.MAX_HEIGHT); + int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = yPositions.BinarySearch(Current + drawHeight); + int lastIndex = yPositions.BinarySearch(visibleBottomBound); if (lastIndex < 0) lastIndex = ~lastIndex; int notVisibleCount = 0; @@ -486,9 +512,8 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). - float halfHeight = drawHeight / 2; foreach (DrawableCarouselItem p in scrollableContent.Children) - updateItem(p, halfHeight); + updateItem(p); } protected override void Dispose(bool isDisposing) @@ -542,7 +567,7 @@ namespace osu.Game.Screens.Select yPositions.Clear(); - float currentY = DrawHeight / 2; + float currentY = visibleHalfHeight; DrawableCarouselBeatmapSet lastSet = null; scrollTarget = null; @@ -575,7 +600,6 @@ namespace osu.Game.Screens.Select float? setY = null; if (!d.IsLoaded || beatmap.Alpha == 0) // can't use IsPresent due to DrawableCarouselItem override. - // ReSharper disable once PossibleNullReferenceException (resharper broken?) setY = lastSet.Y + lastSet.DrawHeight + 5; if (d.IsLoaded) @@ -596,7 +620,7 @@ namespace osu.Game.Screens.Select currentY += d.DrawHeight + 5; } - currentY += DrawHeight / 2; + currentY += visibleHalfHeight; scrollableContent.Height = currentY; if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) @@ -610,7 +634,7 @@ namespace osu.Game.Screens.Select private void updateScrollPosition() { - if (scrollTarget != null) ScrollTo(scrollTarget.Value); + if (scrollTarget != null) scroll.ScrollTo(scrollTarget.Value); scrollPositionCache.Validate(); } @@ -637,18 +661,15 @@ namespace osu.Game.Screens.Select /// the current scroll position. /// /// The item to be updated. - /// Half the draw height of the carousel container. - private void updateItem(DrawableCarouselItem p, float halfHeight) + private void updateItem(DrawableCarouselItem p) { - var height = p.IsPresent ? p.DrawHeight : 0; - - float itemDrawY = p.Position.Y - Current + height / 2; - float dist = Math.Abs(1f - itemDrawY / halfHeight); + float itemDrawY = p.Position.Y - visibleUpperBound + p.DrawHeight / 2; + float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); // Setting the origin position serves as an additive position on top of potential // local transformation we may want to apply (e.g. when a item gets selected, we // may want to smoothly transform it leftwards.) - p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); + p.OriginPosition = new Vector2(-offsetX(dist, visibleHalfHeight), 0); // We are applying a multiplicative alpha (which is internally done by nesting an // additional container and setting that container's alpha) such that we can @@ -673,5 +694,35 @@ namespace osu.Game.Screens.Select base.PerformSelection(); } } + + private class CarouselScrollContainer : OsuScrollContainer + { + private bool rightMouseScrollBlocked; + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // we need to block right click absolute scrolling when hovering a carousel item so context menus can display. + // this can be reconsidered when we have an alternative to right click scrolling. + if (GetContainingInputManager().HoveredDrawables.OfType().Any()) + { + rightMouseScrollBlocked = true; + return false; + } + } + + rightMouseScrollBlocked = false; + return base.OnMouseDown(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + if (rightMouseScrollBlocked) + return false; + + return base.OnDragStart(e); + } + } } } diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs index f66cd2b29a..7f82d3cc12 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs @@ -25,22 +25,6 @@ namespace osu.Game.Screens.Select private Bindable selectedTab; - private void invokeOnFilter() - { - OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colour, OsuConfigManager config) - { - modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight; - - selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab); - - tabs.Current.BindTo(selectedTab); - tabs.Current.TriggerChange(); - } - public BeatmapDetailAreaTabControl() { Height = HEIGHT; @@ -66,12 +50,31 @@ namespace osu.Game.Screens.Select Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Text = @"Mods", + Alpha = 0, }, }; tabs.Current.ValueChanged += _ => invokeOnFilter(); modsCheckbox.Current.ValueChanged += _ => invokeOnFilter(); } + + [BackgroundDependencyLoader] + private void load(OsuColour colour, OsuConfigManager config) + { + modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight; + + selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab); + + tabs.Current.BindTo(selectedTab); + tabs.Current.TriggerChange(); + } + + private void invokeOnFilter() + { + OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value); + + modsCheckbox.FadeTo(tabs.Current.Value == BeatmapDetailTab.Details ? 0 : 1, 200, Easing.OutQuint); + } } public enum BeatmapDetailTab diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 23dd87d8ea..577d999388 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Select private readonly MetadataSection description, source, tags; private readonly Container failRetryContainer; private readonly FailRetryGraph failRetryGraph; - private readonly DimmedLoadingAnimation loading; + private readonly DimmedLoadingLayer loading; private IAPIProvider api; @@ -156,10 +156,7 @@ namespace osu.Game.Screens.Select }, }, }, - loading = new DimmedLoadingAnimation - { - RelativeSizeAxes = Axes.Both, - }, + loading = new DimmedLoadingLayer(), }; } @@ -365,35 +362,5 @@ namespace osu.Game.Screens.Select }); } } - - private class DimmedLoadingAnimation : VisibilityContainer - { - private readonly LoadingAnimation loading; - - public DimmedLoadingAnimation() - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - }, - loading = new LoadingAnimation(), - }; - } - - protected override void PopIn() - { - this.FadeIn(transition_duration, Easing.OutQuint); - loading.Show(); - } - - protected override void PopOut() - { - this.FadeOut(transition_duration, Easing.OutQuint); - loading.Hide(); - } - } } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2551ffe2fc..5f6307e3b4 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -141,6 +141,7 @@ namespace osu.Game.Screens.Select private readonly RulesetInfo ruleset; public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset) + : base(pixelSnapping: true) { this.beatmap = beatmap; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; @@ -152,7 +153,6 @@ namespace osu.Game.Screens.Select var beatmapInfo = beatmap.BeatmapInfo; var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); - PixelSnapping = true; CacheDrawnFrameBuffer = true; RelativeSizeAxes = Axes.Both; @@ -402,31 +402,35 @@ namespace osu.Game.Screens.Select } } - private class DifficultyColourBar : DifficultyColouredContainer + private class DifficultyColourBar : Container { + private readonly BeatmapInfo beatmap; + public DifficultyColourBar(BeatmapInfo beatmap) - : base(beatmap) { + this.beatmap = beatmap; } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { const float full_opacity_ratio = 0.7f; + var difficultyColour = colours.ForDifficultyRating(beatmap.DifficultyRating); + Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = AccentColour, + Colour = difficultyColour, Width = full_opacity_ratio, }, new Box { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, - Colour = AccentColour, + Colour = difficultyColour, Alpha = 0.5f, X = full_opacity_ratio, Width = 1 - full_opacity_ratio, diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 0a20f2aa6d..fba7a328c1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.CentreLeft, Children = new Drawable[] { - new DifficultyIcon(beatmap) + new DifficultyIcon(beatmap, shouldShowTooltip: false) { Scale = new Vector2(1.8f), }, diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 4ceb82d4cc..97b6a78804 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -95,10 +96,11 @@ namespace osu.Game.Screens.Select.Carousel TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, Status = beatmapSet.Status }, - new FillFlowContainer + new FillFlowContainer { AutoSizeAxes = Axes.Both, - Children = ((CarouselBeatmapSet)Item).Beatmaps.Select(b => new FilterableDifficultyIcon(b)).ToList() + Spacing = new Vector2(3), + ChildrenEnumerable = getDifficultyIcons(), }, } } @@ -107,6 +109,17 @@ namespace osu.Game.Screens.Select.Carousel }; } + private const int maximum_difficulty_icons = 18; + + private IEnumerable getDifficultyIcons() + { + var beatmaps = ((CarouselBeatmapSet)Item).Beatmaps.ToList(); + + return beatmaps.Count > maximum_difficulty_icons + ? (IEnumerable)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) + : beatmaps.Select(b => new FilterableDifficultyIcon(b)); + } + public MenuItem[] ContextMenuItems { get @@ -204,5 +217,27 @@ namespace osu.Game.Screens.Select.Carousel filtered.TriggerChange(); } } + + public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon + { + private readonly List items; + + public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) + : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) + { + this.items = items; + + foreach (var item in items) + item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); + + updateFilteredDisplay(); + } + + private void updateFilteredDisplay() + { + // for now, fade the whole group based on the ratio of hidden items. + this.FadeTo(1 - 0.9f * ((float)items.Count(i => i.Filtered.Value) / items.Count), 100); + } + } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index b906bd935c..6118191302 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Select.Carousel { RelativeSizeAxes = Axes.Both, Alpha = 0, - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, }, } }; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index bd0b0dd5cd..ed74b01fc9 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select.Filter; using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -22,46 +21,22 @@ namespace osu.Game.Screens.Select { public class FilterControl : Container { + public const float HEIGHT = 100; + public Action FilterChanged; private readonly OsuTabControl sortTabs; private readonly TabControl groupTabs; - private SortMode sort = SortMode.Title; + private Bindable sortMode; - public SortMode Sort - { - get => sort; - set - { - if (sort != value) - { - sort = value; - FilterChanged?.Invoke(CreateCriteria()); - } - } - } - - private GroupMode group = GroupMode.All; - - public GroupMode Group - { - get => group; - set - { - if (group != value) - { - group = value; - FilterChanged?.Invoke(CreateCriteria()); - } - } - } + private Bindable groupMode; public FilterCriteria CreateCriteria() => new FilterCriteria { - Group = group, - Sort = sort, + Group = groupMode.Value, + Sort = sortMode.Value, SearchText = searchTextBox.Text, AllowConvertedBeatmaps = showConverted.Value, Ruleset = ruleset.Value @@ -120,7 +95,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Height = 24, Width = 0.5f, - AutoSort = true + AutoSort = true, }, //spriteText = new OsuSpriteText //{ @@ -150,8 +125,6 @@ namespace osu.Game.Screens.Select groupTabs.PinItem(GroupMode.All); groupTabs.PinItem(GroupMode.RecentlyPlayed); - groupTabs.Current.ValueChanged += group => Group = group.NewValue; - sortTabs.Current.ValueChanged += sort => Sort = sort.NewValue; } public void Deactivate() @@ -181,15 +154,20 @@ namespace osu.Game.Screens.Select showConverted.ValueChanged += _ => updateCriteria(); ruleset.BindTo(parentRuleset); - ruleset.BindValueChanged(_ => updateCriteria(), true); + ruleset.BindValueChanged(_ => updateCriteria()); + + sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); + groupMode = config.GetBindable(OsuSetting.SongSelectGroupingMode); + + sortTabs.Current.BindTo(sortMode); + groupTabs.Current.BindTo(groupMode); + + groupMode.BindValueChanged(_ => updateCriteria()); + sortMode.BindValueChanged(_ => updateCriteria()); + + updateCriteria(); } private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); - - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override bool OnMouseMove(MouseMoveEvent e) => true; - - protected override bool OnClick(ClickEvent e) => true; } } diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 0680711f1c..1dc7081c1c 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Select buttons = new FillFlowContainer { Direction = FillDirection.Horizontal, - Spacing = new Vector2(0.2f, 0), + Spacing = new Vector2(-FooterButton.SHEAR_WIDTH, 0), AutoSizeAxes = Axes.Both, } } diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index e18a086a10..c1478aa4ce 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -17,7 +17,9 @@ namespace osu.Game.Screens.Select { public class FooterButton : OsuClickableContainer { - private static readonly Vector2 shearing = new Vector2(0.15f, 0); + public static readonly float SHEAR_WIDTH = 7.5f; + + protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); public string Text { @@ -59,37 +61,35 @@ namespace osu.Game.Screens.Select private readonly Box box; private readonly Box light; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); - public FooterButton() { AutoSizeAxes = Axes.Both; + Shear = SHEAR; Children = new Drawable[] { - TextContainer = new Container - { - Size = new Vector2(100, 50), - Child = SpriteText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }, box = new Box { RelativeSizeAxes = Axes.Both, - Shear = shearing, EdgeSmoothness = new Vector2(2, 0), Colour = Color4.White, Alpha = 0, }, light = new Box { - Shear = shearing, Height = 4, EdgeSmoothness = new Vector2(2, 0), RelativeSizeAxes = Axes.X, }, + TextContainer = new Container + { + Size = new Vector2(100 - SHEAR_WIDTH, 50), + Shear = -SHEAR, + Child = SpriteText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, }; } diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index c96c5022c0..29b1364944 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -9,22 +9,30 @@ using osu.Game.Rulesets.Mods; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osuTK; using osuTK.Input; namespace osu.Game.Screens.Select { - public class FooterButtonMods : FooterButton + public class FooterButtonMods : FooterButton, IHasCurrentValue> { - public FooterButtonMods(Bindable> mods) + public Bindable> Current { - FooterModDisplay modDisplay; + get => modDisplay.Current; + set => modDisplay.Current = value; + } + private readonly FooterModDisplay modDisplay; + + public FooterButtonMods() + { Add(new Container { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Shear = -SHEAR, Child = modDisplay = new FooterModDisplay { DisplayUnrankedText = false, @@ -33,9 +41,6 @@ namespace osu.Game.Screens.Select AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Left = 70 } }); - - if (mods != null) - modDisplay.Current = mods; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 0f6d4f3188..cb45c00f66 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -79,6 +79,8 @@ namespace osu.Game.Screens.Select.Leaderboards }; } + protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; + protected override APIRequest FetchScores(Action> scoresCallback) { if (Scope == BeatmapLeaderboardScope.Local) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs index 9e480b61c6..dc4c2ba4e2 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScope.cs @@ -1,13 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Screens.Select.Leaderboards { public enum BeatmapLeaderboardScope { + [Description("Local Ranking")] Local, + + [Description("Country Ranking")] Country, + + [Description("Global Ranking")] Global, + + [Description("Friend Ranking")] Friend, } } diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index a8b5bbbd00..ff9beafb23 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Select.Options { RelativeSizeAxes = Axes.Both, EdgeSmoothness = new Vector2(1.5f, 0), - Blending = BlendingMode.Additive, + Blending = BlendingParameters.Additive, Colour = Color4.White, Alpha = 0, }, diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 669264cef0..ede526f9da 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -110,8 +110,7 @@ namespace osu.Game.Screens.Select.Options HotKey = hotkey }; - buttonsContainer.Add(button); - buttonsContainer.SetLayoutPosition(button, depth); + buttonsContainer.Insert((int)depth, button); } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 0eeffda5eb..edb0e6deb8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Select Size = new Vector2(wedged_container_size.X, 1), Padding = new MarginPadding { - Bottom = 50, + Bottom = Footer.HEIGHT, Top = wedged_container_size.Y + left_area_padding, Left = left_area_padding, Right = left_area_padding * 2, @@ -147,20 +147,28 @@ namespace osu.Game.Screens.Select Width = 0.5f, Children = new Drawable[] { - Carousel = new BeatmapCarousel + new Container { - Masking = false, RelativeSizeAxes = Axes.Both, - Size = new Vector2(1 - wedged_container_size.X, 1), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - SelectionChanged = updateSelectedBeatmap, - BeatmapSetsChanged = carouselBeatmapsLoaded, + Padding = new MarginPadding + { + Top = FilterControl.HEIGHT, + Bottom = Footer.HEIGHT + }, + Child = Carousel = new BeatmapCarousel + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1 - wedged_container_size.X, 1), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + SelectionChanged = updateSelectedBeatmap, + BeatmapSetsChanged = carouselBeatmapsLoaded, + }, }, FilterControl = new FilterControl { RelativeSizeAxes = Axes.X, - Height = 100, + Height = FilterControl.HEIGHT, FilterChanged = c => Carousel.Filter(c), Background = { Width = 2 }, Exit = () => @@ -221,11 +229,9 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader(true)] private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores) { - mods.BindTo(Mods); - if (Footer != null) { - Footer.AddButton(new FooterButtonMods(mods), ModSelect); + Footer.AddButton(new FooterButtonMods { Current = mods }, ModSelect); Footer.AddButton(new FooterButtonRandom { Action = triggerRandom }); Footer.AddButton(new FooterButtonOptions(), BeatmapOptions); @@ -253,7 +259,7 @@ namespace osu.Game.Screens.Select Schedule(() => { // if we have no beatmaps but osu-stable is found, let's prompt the user to import. - if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable) + if (!beatmaps.GetAllUsableBeatmapSetsEnumerable().Any() && beatmaps.StableInstallationAvailable) dialogOverlay.Push(new ImportFromStablePopup(() => { Task.Run(beatmaps.ImportFromStableAsync).ContinueWith(_ => scores.ImportFromStableAsync(), TaskContinuationOptions.OnlyOnRanToCompletion); @@ -263,6 +269,13 @@ namespace osu.Game.Screens.Select } } + protected override void LoadComplete() + { + base.LoadComplete(); + + mods.BindTo(Mods); + } + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -329,7 +342,7 @@ namespace osu.Game.Screens.Select if (this.IsCurrentScreen() && !Carousel.SelectBeatmap(e.NewValue?.BeatmapInfo, false)) // If selecting new beatmap without bypassing filters failed, there's possibly a ruleset mismatch - if (e.NewValue?.BeatmapInfo?.Ruleset != null && e.NewValue.BeatmapInfo.Ruleset != decoupledRuleset.Value) + if (e.NewValue?.BeatmapInfo?.Ruleset != null && !e.NewValue.BeatmapInfo.Ruleset.Equals(decoupledRuleset.Value)) { Ruleset.Value = e.NewValue.BeatmapInfo.Ruleset; Carousel.SelectBeatmap(e.NewValue.BeatmapInfo); @@ -346,6 +359,7 @@ namespace osu.Game.Screens.Select return; beatmapNoDebounce = beatmap; + performUpdateSelected(); } @@ -573,10 +587,18 @@ namespace osu.Game.Screens.Select { Track track = Beatmap.Value.Track; - if ((!track.IsRunning || restart) && music?.IsUserPaused != true) + if (!track.IsRunning || restart) { track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - track.Restart(); + + if (music != null) + { + // use the global music controller (when available) to cancel a potential local user paused state. + music.SeekTo(track.RestartPoint); + music.Play(); + } + else + track.Restart(); } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index c7556dddd5..6072bb64ed 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -4,6 +4,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; +using osu.Game.Audio; namespace osu.Game.Skinning { @@ -19,6 +20,6 @@ namespace osu.Game.Skinning public override Texture GetTexture(string componentName) => null; - public override SampleChannel GetSample(string sampleName) => null; + public override SampleChannel GetSample(ISampleInfo sampleInfo) => null; } } diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 0e67a1897c..4867aba0a9 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; +using osu.Game.Audio; namespace osu.Game.Skinning { @@ -17,7 +18,7 @@ namespace osu.Game.Skinning Texture GetTexture(string componentName); - SampleChannel GetSample(string sampleName); + SampleChannel GetSample(ISampleInfo sampleInfo); TValue GetValue(Func query) where TConfiguration : SkinConfiguration; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 513a024a36..570ba1ced7 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -6,15 +6,26 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Text; +using osu.Game.Audio; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.UI; using osuTK; +using osuTK.Graphics; namespace osu.Game.Skinning { @@ -24,11 +35,22 @@ namespace osu.Game.Skinning protected IResourceStore Samples; + /// + /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc. + /// Their hittable area is 128px, but the actual circle portion is 118px. + /// We must account for some gameplay elements such as slider bodies, where this padding is not present. + /// + private const float legacy_circle_radius = 64 - 5; + public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") { + // defaults should only be applied for non-beatmap skins (which are parsed via this constructor). + if (!Configuration.CustomColours.ContainsKey("SliderBall")) Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); } + private readonly bool hasHitCircle; + protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) : base(skin) { @@ -41,6 +63,14 @@ namespace osu.Game.Skinning Samples = audioManager.GetSampleStore(storage); Textures = new TextureStore(new TextureLoaderStore(storage)); + + using (var testStream = storage.GetStream("hitcircle@2x") ?? storage.GetStream("hitcircle")) + hasHitCircle |= testStream != null; + + if (hasHitCircle) + { + Configuration.SliderPathRadius = legacy_circle_radius; + } } protected override void Dispose(bool isDisposing) @@ -50,30 +80,77 @@ namespace osu.Game.Skinning Samples?.Dispose(); } + private const double default_frame_time = 1000 / 60d; + public override Drawable GetDrawableComponent(string componentName) { + bool animatable = false; + bool looping = true; + switch (componentName) { + case "Play/osu/cursor": + if (GetTexture("cursor") != null) + return new LegacyCursor(); + + return null; + + case "Play/osu/sliderball": + var sliderBallContent = getAnimation("sliderb", true, true, ""); + + if (sliderBallContent != null) + { + var size = sliderBallContent.Size; + + sliderBallContent.RelativeSizeAxes = Axes.Both; + sliderBallContent.Size = Vector2.One; + + return new LegacySliderBall(sliderBallContent) + { + Size = size + }; + } + + return null; + + case "Play/osu/hitcircle": + if (hasHitCircle) + return new LegacyMainCirclePiece(); + + return null; + + case "Play/osu/sliderfollowcircle": + animatable = true; + break; + case "Play/Miss": componentName = "hit0"; + animatable = true; + looping = false; break; case "Play/Meh": componentName = "hit50"; + animatable = true; + looping = false; break; case "Play/Good": componentName = "hit100"; + animatable = true; + looping = false; break; case "Play/Great": componentName = "hit300"; + animatable = true; + looping = false; break; case "Play/osu/number-text": return !hasFont(Configuration.HitCircleFont) ? null - : new LegacySpriteText(Textures, Configuration.HitCircleFont) + : new LegacySpriteText(this, Configuration.HitCircleFont) { Scale = new Vector2(0.96f), // Spacing value was reverse-engineered from the ratio of the rendered sprite size in the visual inspector vs the actual texture size @@ -81,21 +158,14 @@ namespace osu.Game.Skinning }; } - // temporary allowance is given for skins the fact that stable handles non-animatable items such as hitcircles (incorrectly) - // by (incorrectly) displaying the first frame of animation rather than the non-animated version. - // users have used this to "hide" certain elements like hit300. - var texture = GetTexture($"{componentName}-0") ?? GetTexture(componentName); - - if (texture == null) - return null; - - return new Sprite { Texture = texture }; + return getAnimation(componentName, animatable, looping); } public override Texture GetTexture(string componentName) { - float ratio = 2; + componentName = getFallbackName(componentName); + float ratio = 2; var texture = Textures.Get($"{componentName}@2x"); if (texture == null) @@ -105,15 +175,71 @@ namespace osu.Game.Skinning } if (texture != null) - texture.ScaleAdjust = ratio / 0.72f; // brings sizing roughly in-line with stable + texture.ScaleAdjust = ratio; return texture; } - public override SampleChannel GetSample(string sampleName) => Samples.Get(sampleName); + public override SampleChannel GetSample(ISampleInfo sampleInfo) + { + foreach (var lookup in sampleInfo.LookupNames) + { + var sample = Samples.Get(getFallbackName(lookup)); + + if (sample != null) + return sample; + } + + if (sampleInfo is HitSampleInfo hsi) + // Try fallback to non-bank samples. + return Samples.Get(hsi.Name); + + return null; + } private bool hasFont(string fontName) => GetTexture($"{fontName}-0") != null; + private string getFallbackName(string componentName) + { + string lastPiece = componentName.Split('/').Last(); + return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece; + } + + private Drawable getAnimation(string componentName, bool animatable, bool looping, string animationSeparator = "-") + { + Texture texture; + + Texture getFrameTexture(int frame) => GetTexture($"{componentName}{animationSeparator}{frame}"); + + TextureAnimation animation = null; + + if (animatable) + { + for (int i = 0;; i++) + { + if ((texture = getFrameTexture(i)) == null) + break; + + if (animation == null) + animation = new TextureAnimation + { + DefaultFrameLength = default_frame_time, + Repeat = looping + }; + + animation.AddFrame(texture); + } + } + + if (animation != null) + return animation; + + if ((texture = GetTexture(componentName)) != null) + return new Sprite { Texture = texture }; + + return null; + } + protected class LegacySkinResourceStore : IResourceStore where T : INamedFileInfo { @@ -124,11 +250,8 @@ namespace osu.Game.Skinning { bool hasExtension = filename.Contains('.'); - string lastPiece = filename.Split('/').Last(); - var legacyName = filename.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece; - var file = source.Files.Find(f => - string.Equals(hasExtension ? f.Filename : Path.ChangeExtension(f.Filename, null), legacyName, StringComparison.InvariantCultureIgnoreCase)); + string.Equals(hasExtension ? f.Filename : Path.ChangeExtension(f.Filename, null), filename, StringComparison.InvariantCultureIgnoreCase)); return file?.FileInfo.StoragePath; } @@ -182,37 +305,186 @@ namespace osu.Game.Skinning private class LegacySpriteText : OsuSpriteText { - private readonly TextureStore textures; - private readonly string font; + private readonly LegacyGlyphStore glyphStore; - public LegacySpriteText(TextureStore textures, string font) + public LegacySpriteText(ISkin skin, string font) { - this.textures = textures; - this.font = font; - Shadow = false; UseFullGlyphHeight = false; + + Font = new FontUsage(font, OsuFont.DEFAULT_FONT_SIZE); + glyphStore = new LegacyGlyphStore(skin); } - protected override Texture GetTextureForCharacter(char c) + protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); + + private class LegacyGlyphStore : ITexturedGlyphLookupStore { - string textureName = $"{font}-{c}"; + private readonly ISkin skin; - // Approximate value that brings character sizing roughly in-line with stable - float ratio = 36; - - var texture = textures.Get($"{textureName}@2x"); - - if (texture == null) + public LegacyGlyphStore(ISkin skin) { - ratio = 18; - texture = textures.Get(textureName); + this.skin = skin; } - if (texture != null) - texture.ScaleAdjust = ratio; + public ITexturedCharacterGlyph Get(string fontName, char character) + { + var texture = skin.GetTexture($"{fontName}-{character}"); - return texture; + if (texture != null) + // Approximate value that brings character sizing roughly in-line with stable + texture.ScaleAdjust *= 18; + + if (texture == null) + return null; + + return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, null), texture, 1f / texture.ScaleAdjust); + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + } + } + + public class LegacyCursor : CompositeDrawable + { + public LegacyCursor() + { + Size = new Vector2(50); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + InternalChildren = new Drawable[] + { + new NonPlayfieldSprite + { + Texture = skin.GetTexture("cursormiddle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new NonPlayfieldSprite + { + Texture = skin.GetTexture("cursor"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + } + + public class LegacySliderBall : CompositeDrawable + { + private readonly Drawable animationContent; + + public LegacySliderBall(Drawable animationContent) + { + this.animationContent = animationContent; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, DrawableHitObject drawableObject) + { + animationContent.Colour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White; + + InternalChildren = new[] + { + new Sprite + { + Texture = skin.GetTexture("sliderb-nd"), + Colour = new Color4(5, 5, 5, 255), + }, + animationContent, + new Sprite + { + Texture = skin.GetTexture("sliderb-spec"), + Blending = BlendingParameters.Additive, + }, + }; + } + } + + public class LegacyMainCirclePiece : CompositeDrawable + { + public LegacyMainCirclePiece() + { + Size = new Vector2(128); + } + + private readonly IBindable state = new Bindable(); + + private readonly Bindable accentColour = new Bindable(); + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject, ISkinSource skin) + { + Sprite hitCircleSprite; + + InternalChildren = new Drawable[] + { + hitCircleSprite = new Sprite + { + Texture = skin.GetTexture("hitcircle"), + Colour = drawableObject.AccentColour.Value, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 40), + UseFullGlyphHeight = false, + }, confineMode: ConfineMode.NoScaling) + { + Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString() + }, + new Sprite + { + Texture = skin.GetTexture("hitcircleoverlay"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + + state.BindTo(drawableObject.State); + state.BindValueChanged(updateState, true); + + accentColour.BindTo(drawableObject.AccentColour); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + } + + private void updateState(ValueChangedEvent state) + { + const double legacy_fade_duration = 240; + + switch (state.NewValue) + { + case ArmedState.Hit: + this.FadeOut(legacy_fade_duration, Easing.Out); + this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + break; + } + } + } + + /// + /// A sprite which is displayed within the playfield, but historically was not considered part of the playfield. + /// Performs scale adjustment to undo the scale applied by (osu! ruleset specifically). + /// + private class NonPlayfieldSprite : Sprite + { + public override Texture Texture + { + get => base.Texture; + set + { + if (value != null) + // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. + value.ScaleAdjust *= 1.6f; + base.Texture = value; + } } } } diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/LocalSkinOverrideContainer.cs index 37f4cc28a2..fc36d1c8da 100644 --- a/osu.Game/Skinning/LocalSkinOverrideContainer.cs +++ b/osu.Game/Skinning/LocalSkinOverrideContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; +using osu.Game.Audio; using osu.Game.Configuration; namespace osu.Game.Skinning @@ -34,7 +35,7 @@ namespace osu.Game.Skinning public Drawable GetDrawableComponent(string componentName) { Drawable sourceDrawable; - if (beatmapSkins.Value && (sourceDrawable = skin.GetDrawableComponent(componentName)) != null) + if (beatmapSkins.Value && (sourceDrawable = skin?.GetDrawableComponent(componentName)) != null) return sourceDrawable; return fallbackSource?.GetDrawableComponent(componentName); @@ -43,19 +44,19 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName) { Texture sourceTexture; - if (beatmapSkins.Value && (sourceTexture = skin.GetTexture(componentName)) != null) + if (beatmapSkins.Value && (sourceTexture = skin?.GetTexture(componentName)) != null) return sourceTexture; return fallbackSource.GetTexture(componentName); } - public SampleChannel GetSample(string sampleName) + public SampleChannel GetSample(ISampleInfo sampleInfo) { SampleChannel sourceChannel; - if (beatmapHitsounds.Value && (sourceChannel = skin.GetSample(sampleName)) != null) + if (beatmapHitsounds.Value && (sourceChannel = skin?.GetSample(sampleInfo)) != null) return sourceChannel; - return fallbackSource?.GetSample(sampleName); + return fallbackSource?.GetSample(sampleInfo); } public TValue GetValue(Func query) where TConfiguration : SkinConfiguration diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 09c0d3d0bc..027d9df8b8 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; +using osu.Game.Audio; namespace osu.Game.Skinning { @@ -16,7 +17,7 @@ namespace osu.Game.Skinning public abstract Drawable GetDrawableComponent(string componentName); - public abstract SampleChannel GetSample(string sampleName); + public abstract SampleChannel GetSample(ISampleInfo sampleInfo); public abstract Texture GetTexture(string componentName); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 043622f8ce..93b599f9f6 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -27,6 +27,8 @@ namespace osu.Game.Skinning public float? SliderBorderSize { get; set; } + public float? SliderPathRadius { get; set; } + public bool? CursorExpand { get; set; } = true; } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 70abfac501..e747a8b1ce 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -15,6 +15,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; +using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO.Archives; @@ -45,7 +46,7 @@ namespace osu.Game.Skinning CurrentSkinInfo.Value = SkinInfo.Default; }; - CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = getSkin(skin.NewValue); + CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); CurrentSkin.ValueChanged += skin => { if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value) @@ -80,7 +81,7 @@ namespace osu.Game.Skinning { await base.Populate(model, archive, cancellationToken); - Skin reference = getSkin(model); + Skin reference = GetSkin(model); if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name)) { @@ -99,7 +100,7 @@ namespace osu.Game.Skinning /// /// The skin to lookup. /// A instance correlating to the provided . - private Skin getSkin(SkinInfo skinInfo) + public Skin GetSkin(SkinInfo skinInfo) { if (skinInfo == SkinInfo.Default) return new DefaultSkin(); @@ -120,7 +121,7 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName) => CurrentSkin.Value.GetTexture(componentName); - public SampleChannel GetSample(string sampleName) => CurrentSkin.Value.GetSample(sampleName); + public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => CurrentSkin.Value.GetValue(query); } diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index c09d5b1f92..4bbdeafba5 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -36,12 +36,15 @@ namespace osu.Game.Skinning skin.SourceChanged += onChange; } - private void onChange() => SkinChanged(skin, allowDefaultFallback); + private void onChange() => + // schedule required to avoid calls after disposed. + // note that this has the side-effect of components only performing a skin change when they are alive. + Scheduler.AddOnce(() => SkinChanged(skin, allowDefaultFallback)); protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); - onChange(); + SkinChanged(skin, allowDefaultFallback); } /// diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 995cb15136..0c635a3d2f 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -1,35 +1,26 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Caching; using osu.Framework.Graphics; using osuTK; namespace osu.Game.Skinning { - public class SkinnableDrawable : SkinnableDrawable - { - public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : base(name, defaultImplementation, allowFallback, restrictSize) - { - } - } - /// /// A drawable which can be skinned via an . /// - /// The type of drawable. - public class SkinnableDrawable : SkinReloadableDrawable - where T : Drawable + public class SkinnableDrawable : SkinReloadableDrawable { /// - /// The displayed component. May or may not be a type- member. + /// The displayed component. /// protected Drawable Drawable { get; private set; } private readonly string componentName; - private readonly bool restrictSize; + private readonly ConfineMode confineMode; /// /// Create a new skinnable drawable. @@ -37,28 +28,32 @@ namespace osu.Game.Skinning /// The namespace-complete resource name for this skinnable element. /// A function to create the default skin implementation of this element. /// A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present. - /// Whether a user-skin drawable should be limited to the size of our parent. - public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : this(name, allowFallback, restrictSize) + /// How (if at all) the should be resize to fit within our own bounds. + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : this(name, allowFallback, confineMode) { createDefault = defaultImplementation; } - protected SkinnableDrawable(string name, Func allowFallback = null, bool restrictSize = true) + protected SkinnableDrawable(string name, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) : base(allowFallback) { componentName = name; - this.restrictSize = restrictSize; + this.confineMode = confineMode; RelativeSizeAxes = Axes.Both; } - private readonly Func createDefault; + private readonly Func createDefault; - protected virtual T CreateDefault(string name) => createDefault(name); + private readonly Cached scaling = new Cached(); + + private bool isDefault; + + protected virtual Drawable CreateDefault(string name) => createDefault(name); /// - /// Whether to apply size restrictions (specified via ) to the default implementation. + /// Whether to apply size restrictions (specified via ) to the default implementation. /// protected virtual bool ApplySizeRestrictionsToDefault => false; @@ -66,7 +61,7 @@ namespace osu.Game.Skinning { Drawable = skin.GetDrawableComponent(componentName); - bool isDefault = false; + isDefault = false; if (Drawable == null && allowFallback) { @@ -76,21 +71,57 @@ namespace osu.Game.Skinning if (Drawable != null) { - if (restrictSize && (!isDefault || ApplySizeRestrictionsToDefault)) - { - Drawable.RelativeSizeAxes = Axes.Both; - Drawable.Size = Vector2.One; - Drawable.Scale = Vector2.One; - Drawable.FillMode = FillMode.Fit; - } - + scaling.Invalidate(); Drawable.Origin = Anchor.Centre; Drawable.Anchor = Anchor.Centre; - InternalChild = Drawable; } else ClearInternal(); } + + protected override void Update() + { + base.Update(); + + if (!scaling.IsValid) + { + try + { + if (Drawable == null || (isDefault && !ApplySizeRestrictionsToDefault)) return; + + switch (confineMode) + { + case ConfineMode.NoScaling: + return; + + case ConfineMode.ScaleDownToFit: + if (Drawable.DrawSize.X <= DrawSize.X && Drawable.DrawSize.Y <= DrawSize.Y) + return; + + break; + } + + Drawable.RelativeSizeAxes = Axes.Both; + Drawable.Size = Vector2.One; + Drawable.Scale = Vector2.One; + Drawable.FillMode = FillMode.Fit; + } + finally + { + scaling.Validate(); + } + } + } + } + + public enum ConfineMode + { + /// + /// Don't apply any scaling. This allows the user element to be of any size, exceeding specified bounds. + /// + NoScaling, + ScaleDownToFit, + ScaleToFit, } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 8e2b5cec98..dcb14b4412 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -37,34 +36,26 @@ namespace osu.Game.Skinning public void Play() => channels?.ForEach(c => c.Play()); - public override bool IsPresent => false; // We don't need to receive updates. + public override bool IsPresent => Scheduler.HasPendingTasks; protected override void SkinChanged(ISkinSource skin, bool allowFallback) { channels = hitSamples.Select(s => { - var ch = loadChannel(s, skin.GetSample); + var ch = skin.GetSample(s); + if (ch == null && allowFallback) - ch = loadChannel(s, audio.Samples.Get); + foreach (var lookup in s.LookupNames) + if ((ch = audio.Samples.Get($"Gameplay/{lookup}")) != null) + break; + + if (ch != null) + ch.Volume.Value = s.Volume / 100.0; + return ch; }).Where(c => c != null).ToArray(); } - private SampleChannel loadChannel(ISampleInfo info, Func getSampleFunction) - { - foreach (var lookup in info.LookupNames) - { - var ch = getSampleFunction($"Gameplay/{lookup}"); - if (ch == null) - continue; - - ch.Volume.Value = info.Volume / 100.0; - return ch; - } - - return null; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index ceb1ed0f70..07ba48d6ae 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -11,18 +12,18 @@ namespace osu.Game.Skinning /// /// A skinnable element which uses a stable sprite and can therefore share implementation logic. /// - public class SkinnableSprite : SkinnableDrawable + public class SkinnableSprite : SkinnableDrawable { protected override bool ApplySizeRestrictionsToDefault => true; [Resolved] private TextureStore textures { get; set; } - public SkinnableSprite(string name, Func allowFallback = null, bool restrictSize = true) - : base(name, allowFallback, restrictSize) + public SkinnableSprite(string name, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, allowFallback, confineMode) { } - protected override Sprite CreateDefault(string name) => new Sprite { Texture = textures.Get(name) }; + protected override Drawable CreateDefault(string name) => new Sprite { Texture = textures.Get(name) }; } } diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs index 36e646d743..5af6df15e1 100644 --- a/osu.Game/Skinning/SkinnableSpriteText.cs +++ b/osu.Game/Skinning/SkinnableSpriteText.cs @@ -6,10 +6,10 @@ using osu.Framework.Graphics.Sprites; namespace osu.Game.Skinning { - public class SkinnableSpriteText : SkinnableDrawable, IHasText + public class SkinnableSpriteText : SkinnableDrawable, IHasText { - public SkinnableSpriteText(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) - : base(name, defaultImplementation, allowFallback, restrictSize) + public SkinnableSpriteText(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + : base(name, defaultImplementation, allowFallback, confineMode) { } diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs index aa1f137cf3..bcf642b4ea 100644 --- a/osu.Game/Storyboards/CommandTimeline.cs +++ b/osu.Game/Storyboards/CommandTimeline.cs @@ -15,10 +15,10 @@ namespace osu.Game.Storyboards public IEnumerable Commands => commands.OrderBy(c => c.StartTime); public bool HasCommands => commands.Count > 0; - private Cached startTimeBacking; + private readonly Cached startTimeBacking = new Cached(); public double StartTime => startTimeBacking.IsValid ? startTimeBacking : startTimeBacking.Value = HasCommands ? commands.Min(c => c.StartTime) : double.MinValue; - private Cached endTimeBacking; + private readonly Cached endTimeBacking = new Cached(); public double EndTime => endTimeBacking.IsValid ? endTimeBacking : endTimeBacking.Value = HasCommands ? commands.Max(c => c.EndTime) : double.MaxValue; public T StartValue => HasCommands ? commands.OrderBy(c => c.StartTime).First().StartValue : default; diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index b1cc0436de..461ee762e9 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -20,7 +20,7 @@ namespace osu.Game.Storyboards public CommandTimeline Rotation = new CommandTimeline(); public CommandTimeline Colour = new CommandTimeline(); public CommandTimeline Alpha = new CommandTimeline(); - public CommandTimeline BlendingMode = new CommandTimeline(); + public CommandTimeline BlendingParameters = new CommandTimeline(); public CommandTimeline FlipH = new CommandTimeline(); public CommandTimeline FlipV = new CommandTimeline(); @@ -35,7 +35,7 @@ namespace osu.Game.Storyboards yield return Rotation; yield return Colour; yield return Alpha; - yield return BlendingMode; + yield return BlendingParameters; yield return FlipH; yield return FlipV; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index ffd238d4e1..b04f1d4518 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.IO; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -17,25 +16,24 @@ namespace osu.Game.Storyboards.Drawables /// private const double allowable_late_start = 100; - private readonly StoryboardSample sample; + private readonly StoryboardSampleInfo sampleInfo; private SampleChannel channel; public override bool RemoveWhenNotAlive => false; - public DrawableStoryboardSample(StoryboardSample sample) + public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) { - this.sample = sample; - LifetimeStart = sample.StartTime; + this.sampleInfo = sampleInfo; + LifetimeStart = sampleInfo.StartTime; } [BackgroundDependencyLoader] private void load(IBindable beatmap) { - // Try first with the full name, then attempt with no path - channel = beatmap.Value.Skin.GetSample(sample.Path) ?? beatmap.Value.Skin.GetSample(Path.ChangeExtension(sample.Path, null)); + channel = beatmap.Value.Skin.GetSample(sampleInfo); if (channel != null) - channel.Volume.Value = sample.Volume / 100; + channel.Volume.Value = sampleInfo.Volume / 100.0; } protected override void Update() @@ -43,27 +41,27 @@ namespace osu.Game.Storyboards.Drawables base.Update(); // TODO: this logic will need to be consolidated with other game samples like hit sounds. - if (Time.Current < sample.StartTime) + if (Time.Current < sampleInfo.StartTime) { // We've rewound before the start time of the sample channel?.Stop(); // In the case that the user fast-forwards to a point far beyond the start time of the sample, // we want to be able to fall into the if-conditional below (therefore we must not have a life time end) - LifetimeStart = sample.StartTime; + LifetimeStart = sampleInfo.StartTime; LifetimeEnd = double.MaxValue; } - else if (Time.Current - Time.Elapsed < sample.StartTime) + else if (Time.Current - Time.Elapsed < sampleInfo.StartTime) { // We've passed the start time of the sample. We only play the sample if we're within an allowable range // from the sample's start, to reduce layering if we've been fast-forwarded far into the future - if (Time.Current - sample.StartTime < allowable_late_start) + if (Time.Current - sampleInfo.StartTime < allowable_late_start) channel?.Play(); // In the case that the user rewinds to a point far behind the start time of the sample, // we want to be able to fall into the if-conditional above (therefore we must not have a life time start) LifetimeStart = double.MinValue; - LifetimeEnd = sample.StartTime; + LifetimeEnd = sampleInfo.StartTime; } } } diff --git a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs index 7e31e1135e..bbc55a336d 100644 --- a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs +++ b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs @@ -12,19 +12,19 @@ namespace osu.Game.Storyboards.Drawables /// Adjusts after a delay. /// /// A to which further transforms can be added. - public static TransformSequence TransformBlendingMode(this T drawable, BlendingMode newValue, double delay = 0) + public static TransformSequence TransformBlendingMode(this T drawable, BlendingParameters newValue, double delay = 0) where T : Drawable - => drawable.TransformTo(drawable.PopulateTransform(new TransformBlendingMode(), newValue, delay)); + => drawable.TransformTo(drawable.PopulateTransform(new TransformBlendingParameters(), newValue, delay)); } - public class TransformBlendingMode : Transform + public class TransformBlendingParameters : Transform { - private BlendingMode valueAt(double time) + private BlendingParameters valueAt(double time) => time < EndTime ? StartValue : EndValue; public override string TargetMember => nameof(Drawable.Blending); protected override void Apply(Drawable d, double time) => d.Blending = valueAt(time); - protected override void ReadIntoStartValue(Drawable d) => StartValue = d.Blending.Mode; + protected override void ReadIntoStartValue(Drawable d) => StartValue = d.Blending; } } diff --git a/osu.Game/Storyboards/StoryboardSample.cs b/osu.Game/Storyboards/StoryboardSample.cs index 24231cdca6..5d6ce215f5 100644 --- a/osu.Game/Storyboards/StoryboardSample.cs +++ b/osu.Game/Storyboards/StoryboardSample.cs @@ -1,21 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Game.Audio; using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards { - public class StoryboardSample : IStoryboardElement + public class StoryboardSampleInfo : IStoryboardElement, ISampleInfo { - public string Path { get; set; } + public string Path { get; } public bool IsDrawable => true; public double StartTime { get; } - public float Volume; + public int Volume { get; } - public StoryboardSample(string path, double time, float volume) + public IEnumerable LookupNames => new[] + { + // Try first with the full name, then attempt with no path + Path, + System.IO.Path.ChangeExtension(Path, null), + }; + + public StoryboardSampleInfo(string path, double time, int volume) { Path = path; StartTime = time; diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 8f8ec22aae..37c3ff495f 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -69,7 +69,7 @@ namespace osu.Game.Storyboards applyCommands(drawable, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.BlendingMode, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), false); + applyCommands(drawable, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), false); if (drawable is IFlippable flippable) { diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 6a5e17eb38..a555a52e42 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -4,13 +4,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Newtonsoft.Json; using NUnit.Framework; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Tests.Beatmaps @@ -25,11 +28,9 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } - protected IBeatmapConverter Converter { get; private set; } - - protected void Test(string name) + protected void Test(string name, params Type[] mods) { - var ourResult = convert(name); + var ourResult = convert(name, mods.Select(m => (Mod)Activator.CreateInstance(m)).ToArray()); var expectedResult = read(name); Assert.Multiple(() => @@ -91,33 +92,40 @@ namespace osu.Game.Tests.Beatmaps }); } - private ConvertResult convert(string name) + private ConvertResult convert(string name, Mod[] mods) { var beatmap = getBeatmap(name); var rulesetInstance = CreateRuleset(); beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); - Converter = rulesetInstance.CreateBeatmapConverter(beatmap); + var converterResult = new Dictionary>(); - var result = new ConvertResult(); - - Converter.ObjectConverted += (orig, converted) => + var working = new ConversionWorkingBeatmap(beatmap) { - converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); - - var mapping = CreateConvertMapping(); - mapping.StartTime = orig.StartTime; - - foreach (var obj in converted) - mapping.Objects.AddRange(CreateConvertValue(obj)); - result.Mappings.Add(mapping); + ConversionGenerated = (o, r, c) => + { + converterResult[o] = r; + OnConversionGenerated(o, r, c); + } }; - IBeatmap convertedBeatmap = Converter.Convert(); - rulesetInstance.CreateBeatmapProcessor(convertedBeatmap)?.PostProcess(); + working.GetPlayableBeatmap(rulesetInstance.RulesetInfo, mods); - return result; + return new ConvertResult + { + Mappings = converterResult.Select(r => + { + var mapping = CreateConvertMapping(r.Key); + mapping.StartTime = r.Key.StartTime; + mapping.Objects.AddRange(r.Value.SelectMany(CreateConvertValue)); + return mapping; + }).ToList() + }; + } + + protected virtual void OnConversionGenerated(HitObject original, IEnumerable result, IBeatmapConverter beatmapConverter) + { } private ConvertResult read(string name) @@ -154,7 +162,7 @@ namespace osu.Game.Tests.Beatmaps /// This should be used to validate the integrity of the conversion process after a conversion has occurred. /// /// - protected virtual TConvertMapping CreateConvertMapping() => new TConvertMapping(); + protected virtual TConvertMapping CreateConvertMapping(HitObject source) => new TConvertMapping(); /// /// Creates the conversion value for a . A conversion value stores information about the converted . @@ -176,6 +184,32 @@ namespace osu.Game.Tests.Beatmaps [JsonProperty] public List Mappings = new List(); } + + private class ConversionWorkingBeatmap : WorkingBeatmap + { + public Action, IBeatmapConverter> ConversionGenerated; + + private readonly IBeatmap beatmap; + + public ConversionWorkingBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetTrack() => throw new NotImplementedException(); + + protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + { + var converter = base.CreateBeatmapConverter(beatmap, ruleset); + converter.ObjectConverted += (orig, converted) => ConversionGenerated?.Invoke(orig, converted, converter); + return converter; + } + } } public abstract class BeatmapConversionTest : BeatmapConversionTest, TConvertValue> diff --git a/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs b/osu.Game/Tests/Visual/OsuGridTestScene.cs similarity index 97% rename from osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs rename to osu.Game/Tests/Visual/OsuGridTestScene.cs index 096ac951de..c09f4d6218 100644 --- a/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGridTestScene.cs @@ -5,7 +5,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual { /// /// An abstract test case which exposes small cells arranged in a grid. diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 9b3c15aa91..27d72f3950 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -15,6 +15,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; @@ -43,6 +44,9 @@ namespace osu.Game.Tests.Visual private readonly Lazy localStorage; protected Storage LocalStorage => localStorage.Value; + private readonly Lazy contextFactory; + protected DatabaseContextFactory ContextFactory => contextFactory.Value; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { // This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures @@ -59,6 +63,14 @@ namespace osu.Game.Tests.Visual protected OsuTestScene() { localStorage = new Lazy(() => new NativeStorage($"{GetType().Name}-{Guid.NewGuid()}")); + contextFactory = new Lazy(() => + { + var factory = new DatabaseContextFactory(LocalStorage); + factory.ResetDatabase(); + using (var usage = factory.Get()) + usage.Migrate(); + return factory; + }); } [Resolved] @@ -85,6 +97,9 @@ namespace osu.Game.Tests.Visual if (beatmap?.Value.TrackLoaded == true) beatmap.Value.Track.Stop(); + if (contextFactory.IsValueCreated) + contextFactory.Value.ResetDatabase(); + if (localStorage.IsValueCreated) { try diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index a49f2d079b..795b90ba11 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Transforms; namespace osu.Game.Users.Drawables { @@ -38,8 +37,6 @@ namespace osu.Game.Users.Drawables set => base.EdgeEffect = value; } - protected override bool TransformImmediately { get; } - /// /// Whether to show a default guest representation on null user (as opposed to nothing). /// @@ -50,14 +47,11 @@ namespace osu.Game.Users.Drawables /// public readonly BindableBool OpenOnClick = new BindableBool(true); - public UpdateableAvatar(User user = null, bool hideImmediately = false) + public UpdateableAvatar(User user = null) { - TransformImmediately = hideImmediately; User = user; } - protected override TransformSequence ApplyHideTransforms(Drawable drawable) => TransformImmediately ? drawable?.FadeOut() : base.ApplyHideTransforms(drawable); - protected override Drawable CreateDrawable(User user) { if (user == null && !ShowGuestOnNull) diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index 78d1a8de20..abc16b2390 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Transforms; namespace osu.Game.Users.Drawables { @@ -15,21 +14,16 @@ namespace osu.Game.Users.Drawables set => Model = value; } - protected override bool TransformImmediately { get; } - /// /// Whether to show a place holder on null country. /// public bool ShowPlaceholderOnNull = true; - public UpdateableFlag(Country country = null, bool hideImmediately = false) + public UpdateableFlag(Country country = null) { - TransformImmediately = hideImmediately; Country = country; } - protected override TransformSequence ApplyHideTransforms(Drawable drawable) => TransformImmediately ? drawable?.FadeOut() : base.ApplyHideTransforms(drawable); - protected override Drawable CreateDrawable(Country country) { if (country == null && !ShowPlaceholderOnNull) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index df41e194b0..9986f70557 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -20,6 +20,9 @@ namespace osu.Game.Users [JsonProperty(@"username")] public string Username; + [JsonProperty(@"previous_usernames")] + public string[] PreviousUsernames; + [JsonProperty(@"country")] public Country Country; @@ -118,7 +121,7 @@ namespace osu.Game.Users public int PostCount; [JsonProperty(@"follower_count")] - public int[] FollowerCount; + public int FollowerCount; [JsonProperty] private string[] playstyle @@ -159,7 +162,10 @@ namespace osu.Game.Users } [JsonProperty(@"rankHistory")] - public RankHistoryData RankHistory; + private RankHistoryData rankHistory + { + set => Statistics.RankHistory = value; + } [JsonProperty("badges")] public Badge[] Badges; @@ -184,6 +190,7 @@ namespace osu.Game.Users public static readonly User SYSTEM_USER = new User { Username = "system", + Colour = @"9c0101", Id = 0 }; diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 7afbef01c5..032ec2e05f 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -4,6 +4,7 @@ using System; using Newtonsoft.Json; using osu.Game.Scoring; +using static osu.Game.Users.User; namespace osu.Game.Users { @@ -113,5 +114,7 @@ namespace osu.Game.Users [JsonProperty(@"country")] public int? Country; } + + public RankHistoryData RankHistory; } } diff --git a/osu.Game/Utils/HumanizerUtils.cs b/osu.Game/Utils/HumanizerUtils.cs new file mode 100644 index 0000000000..5b7c3630d9 --- /dev/null +++ b/osu.Game/Utils/HumanizerUtils.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using Humanizer; + +namespace osu.Game.Utils +{ + public static class HumanizerUtils + { + /// + /// Turns the current or provided date into a human readable sentence + /// + /// The date to be humanized + /// distance of time in words + public static string Humanize(DateTimeOffset input) + { + // this works around https://github.com/xamarin/xamarin-android/issues/2012 and https://github.com/Humanizr/Humanizer/issues/690#issuecomment-368536282 + try + { + return input.Humanize(); + } + catch (ArgumentException) + { + return input.Humanize(culture: new CultureInfo("en-US")); + } + } + } +} diff --git a/osu.Game/Utils/RavenLogger.cs b/osu.Game/Utils/RavenLogger.cs index 7f4faa60ae..16178e63bd 100644 --- a/osu.Game/Utils/RavenLogger.cs +++ b/osu.Game/Utils/RavenLogger.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net; using System.Threading.Tasks; using osu.Framework.Logging; using SharpRaven; @@ -36,31 +37,50 @@ namespace osu.Game.Utils if (exception != null) { - if (exception is IOException ioe) - { - // disk full exceptions, see https://stackoverflow.com/a/9294382 - const int hr_error_handle_disk_full = unchecked((int)0x80070027); - const int hr_error_disk_full = unchecked((int)0x80070070); - - if (ioe.HResult == hr_error_handle_disk_full || ioe.HResult == hr_error_disk_full) - return; - } + if (!shouldSubmitException(exception)) + return; // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports. if (lastException != null && lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace)) - { return; - } lastException = exception; - queuePendingTask(raven.CaptureAsync(new SentryEvent(exception))); + queuePendingTask(raven.CaptureAsync(new SentryEvent(exception) { Message = entry.Message })); } else raven.AddTrail(new Breadcrumb(entry.Target.ToString(), BreadcrumbType.Navigation) { Message = entry.Message }); }; } + private bool shouldSubmitException(Exception exception) + { + switch (exception) + { + case IOException ioe: + // disk full exceptions, see https://stackoverflow.com/a/9294382 + const int hr_error_handle_disk_full = unchecked((int)0x80070027); + const int hr_error_disk_full = unchecked((int)0x80070070); + + if (ioe.HResult == hr_error_handle_disk_full || ioe.HResult == hr_error_disk_full) + return false; + + break; + + case WebException we: + switch (we.Status) + { + // more statuses may need to be blocked as we come across them. + case WebExceptionStatus.Timeout: + return false; + } + + break; + } + + return true; + } + private void queuePendingTask(Task task) { lock (tasks) tasks.Add(task); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e872cd1387..d791909372 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -11,12 +11,12 @@ - - + + - - - + + + diff --git a/osu.iOS.props b/osu.iOS.props index a319094cb1..9fc472bf40 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -3,7 +3,8 @@ {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Resources PackageReference - --nolinkaway -gcc_flags "-lstdc++ -framework AudioToolbox -framework SystemConfiguration -framework CFNetwork -framework Accelerate + --nolinkaway + -lstdc++ -lbz2 -framework AudioToolbox -framework AVFoundation -framework CoreMedia -framework VideoToolbox -framework SystemConfiguration -framework CFNetwork -framework Accelerate true @@ -24,7 +25,7 @@ false Default - $(DefaultMtouchExtraArgs) -force_load $(OutputPath)\libbass.a -force_load $(OutputPath)\libbass_fx.a" + $(DefaultMtouchExtraArgs) -gcc_flags "$(DefaultMtouchGccFlags)" false cjk,mideast,other,rare,west @@ -44,7 +45,7 @@ NSUrlSessionHandler Default - $(DefaultMtouchExtraArgs) -force_load $(OutputPath)\libbass.a -force_load $(OutputPath)\libbass_fx.a" + $(DefaultMtouchExtraArgs) -gcc_flags "$(DefaultMtouchGccFlags)" false cjk,mideast,other,rare,west @@ -62,7 +63,7 @@ NSUrlSessionHandler Default - $(DefaultMtouchExtraArgs) -force_load $(OutputPath)\libbass.a -force_load $(OutputPath)\libbass_fx.a" + $(DefaultMtouchExtraArgs) -gcc_flags "$(DefaultMtouchGccFlags)" false cjk,mideast,other,rare,west @@ -86,10 +87,22 @@ NSUrlSessionHandler Default - $(DefaultMtouchExtraArgs) -force_load $(OutputPath)\libbass.a -force_load $(OutputPath)\libbass_fx.a" + $(DefaultMtouchExtraArgs) -gcc_flags "$(DefaultMtouchGccFlags)" false cjk,mideast,other,rare,west + + + Static + False + True + + + Static + False + True + + @@ -104,13 +117,13 @@ - - - - + + + + - + diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs index 058e246ed8..9ef21e014c 100644 --- a/osu.iOS/AppDelegate.cs +++ b/osu.iOS/AppDelegate.cs @@ -1,15 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using Foundation; using osu.Framework.iOS; using osu.Game; +using UIKit; namespace osu.iOS { [Register("AppDelegate")] public class AppDelegate : GameAppDelegate { - protected override Framework.Game CreateGame() => new OsuGameIOS(); + private OsuGameIOS game; + + protected override Framework.Game CreateGame() => game = new OsuGameIOS(); + + public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation) + { + Task.Run(() => game.Import(url.Path)); + return true; + } } } diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json index bbd5f4f3ba..af4b103867 100644 --- a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,249 +1 @@ -{ - "images": [ - { - "filename": "Icon-App-20x20@2x.png", - "size": "20x20", - "scale": "2x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-20x20@3x.png", - "size": "20x20", - "scale": "3x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-29x29@2x.png", - "size": "29x29", - "scale": "2x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-29x29@3x.png", - "size": "29x29", - "scale": "3x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-40x40@2x.png", - "size": "40x40", - "scale": "2x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-40x40@3x.png", - "size": "40x40", - "scale": "3x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-60x60@2x.png", - "size": "60x60", - "scale": "2x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-60x60@3x.png", - "size": "60x60", - "scale": "3x", - "idiom": "iphone" - }, - { - "filename": "Icon-App-20x20@1x.png", - "size": "20x20", - "scale": "1x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-20x20@2x.png", - "size": "20x20", - "scale": "2x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-29x29@1x.png", - "size": "29x29", - "scale": "1x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-29x29@2x.png", - "size": "29x29", - "scale": "2x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-40x40@1x.png", - "size": "40x40", - "scale": "1x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-40x40@2x.png", - "size": "40x40", - "scale": "2x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-83.5x83.5@2x.png", - "size": "83.5x83.5", - "scale": "2x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-76x76@1x.png", - "size": "76x76", - "scale": "1x", - "idiom": "ipad" - }, - { - "filename": "Icon-App-76x76@2x.png", - "size": "76x76", - "scale": "2x", - "idiom": "ipad" - }, - { - "filename": "ItunesArtwork@2x.png", - "size": "1024x1024", - "scale": "1x", - "idiom": "ios-marketing" - }, - { - "size": "60x60", - "scale": "2x", - "idiom": "car" - }, - { - "size": "60x60", - "scale": "3x", - "idiom": "car" - }, - { - "role": "notificationCenter", - "size": "24x24", - "subtype": "38mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "notificationCenter", - "size": "27.5x27.5", - "subtype": "42mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "companionSettings", - "size": "29x29", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "companionSettings", - "size": "29x29", - "scale": "3x", - "idiom": "watch" - }, - { - "role": "appLauncher", - "size": "40x40", - "subtype": "38mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "appLauncher", - "size": "44x44", - "subtype": "40mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "appLauncher", - "size": "50x50", - "subtype": "44mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "quickLook", - "size": "86x86", - "subtype": "38mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "quickLook", - "size": "98x98", - "subtype": "42mm", - "scale": "2x", - "idiom": "watch" - }, - { - "role": "quickLook", - "size": "108x108", - "subtype": "44mm", - "scale": "2x", - "idiom": "watch" - }, - { - "size": "1024x1024", - "scale": "1x", - "idiom": "watch-marketing" - }, - { - "size": "16x16", - "scale": "1x", - "idiom": "mac" - }, - { - "size": "16x16", - "scale": "2x", - "idiom": "mac" - }, - { - "size": "32x32", - "scale": "1x", - "idiom": "mac" - }, - { - "size": "32x32", - "scale": "2x", - "idiom": "mac" - }, - { - "size": "128x128", - "scale": "1x", - "idiom": "mac" - }, - { - "size": "128x128", - "scale": "2x", - "idiom": "mac" - }, - { - "size": "256x256", - "scale": "1x", - "idiom": "mac" - }, - { - "size": "256x256", - "scale": "2x", - "idiom": "mac" - }, - { - "size": "512x512", - "scale": "1x", - "idiom": "mac" - }, - { - "size": "512x512", - "scale": "2x", - "idiom": "mac" - } - ], - "info": { - "version": 1, - "author": "xcode" - } -} \ No newline at end of file +{"images":[{"size":"20x20","idiom":"iphone","filename":"iPhoneNotification2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"iPhoneNotification3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"iPhoneSettings2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"iPhoneSettings3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"iPhoneSpotlight2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"iPhoneSpotlight3x.png","scale":"3x"},{"size":"60x60","idiom":"iphone","filename":"iPhoneApp2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"iPhoneApp3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"iPadNotification1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"iPadNotification2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"iPadSettings1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"iPadSettings2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"iPadSpotlight1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"iPadSpotlight2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"iPadApp1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"iPadApp2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"iPadProApp2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"iOSAppStore.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index dfcce1297b..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index d563e8cf5e..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 8a21d9f81d..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4477ece73a..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index 1fe354a7fa..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 0f8c36a038..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index d563e8cf5e..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 0208f80e3b..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index f7ad8bb6bb..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index f7ad8bb6bb..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 3d85a133aa..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index edd53bc954..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index b13a6e7a1e..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index edff3788f0..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png deleted file mode 100644 index c04f6f3ec4..0000000000 Binary files a/osu.iOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png and /dev/null differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png new file mode 100644 index 0000000000..0e8bb029bc Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iOSAppStore.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png new file mode 100644 index 0000000000..42fead2364 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp1x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png new file mode 100644 index 0000000000..785db50cb2 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadApp2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png new file mode 100644 index 0000000000..8c483a0a7a Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification1x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png new file mode 100644 index 0000000000..a45b01b91c Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadNotification2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png new file mode 100644 index 0000000000..d2ba8f3a7e Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadProApp2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png new file mode 100644 index 0000000000..43d577040e Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings1x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png new file mode 100644 index 0000000000..1ebec1390b Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSettings2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png new file mode 100644 index 0000000000..a45b01b91c Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight1x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png new file mode 100644 index 0000000000..717603dd68 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPadSpotlight2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png new file mode 100644 index 0000000000..6b61c09db5 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png new file mode 100644 index 0000000000..78ef8d12b7 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneApp3x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png new file mode 100644 index 0000000000..a45b01b91c Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png new file mode 100644 index 0000000000..46ddf1179d Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneNotification3x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png new file mode 100644 index 0000000000..1ebec1390b Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png new file mode 100644 index 0000000000..a8145f0246 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSettings3x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png new file mode 100644 index 0000000000..717603dd68 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight2x.png differ diff --git a/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png new file mode 100644 index 0000000000..6b61c09db5 Binary files /dev/null and b/osu.iOS/Assets.xcassets/AppIcon.appiconset/iPhoneSpotlight3x.png differ diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index d7992353cf..a118b329aa 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -31,6 +31,10 @@ UIStatusBarHidden + NSCameraUsageDescription + We don't really use the camera. + NSMicrophoneUsageDescription + We don't really use the microphone. UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -40,5 +44,70 @@ XSAppIconAssets Assets.xcassets/AppIcon.appiconset + UTExportedTypeDeclarations + + + UTTypeConformsTo + + + + UTTypeIdentifier + sh.ppy.osu.items + UTTypeTagSpecification + + + + UTTypeConformsTo + + sh.ppy.osu.items + + UTTypeIdentifier + sh.ppy.osu.osr + UTTypeTagSpecification + + public.filename-extension + osr + + + + UTTypeConformsTo + + sh.ppy.osu.items + + UTTypeIdentifier + sh.ppy.osu.osk + UTTypeTagSpecification + + public.filename-extension + osk + + + + UTTypeConformsTo + + sh.ppy.osu.items + + UTTypeIdentifier + sh.ppy.osu.osz + UTTypeTagSpecification + + public.filename-extension + osz + + + + CFBundleDocumentTypes + + + LSHandlerRank + Owner + CFBundleTypeName + Supported osu! files + LSItemContentTypes + + sh.ppy.osu.items + + + diff --git a/osu.iOS/iTunesArtwork b/osu.iOS/iTunesArtwork index ef7441433a..1939459992 100644 Binary files a/osu.iOS/iTunesArtwork and b/osu.iOS/iTunesArtwork differ diff --git a/osu.iOS/iTunesArtwork@2x b/osu.iOS/iTunesArtwork@2x index c04f6f3ec4..0e8bb029bc 100644 Binary files a/osu.iOS/iTunesArtwork@2x and b/osu.iOS/iTunesArtwork@2x differ diff --git a/osu.iOS/libbass.a b/osu.iOS/libbass.a deleted file mode 100644 index c34e6a0a0c..0000000000 Binary files a/osu.iOS/libbass.a and /dev/null differ diff --git a/osu.iOS/libbass_fx.a b/osu.iOS/libbass_fx.a deleted file mode 100644 index 007cd28647..0000000000 Binary files a/osu.iOS/libbass_fx.a and /dev/null differ diff --git a/osu.iOS/osu.iOS.csproj b/osu.iOS/osu.iOS.csproj index 9df3ad5516..19d1acf014 100644 --- a/osu.iOS/osu.iOS.csproj +++ b/osu.iOS/osu.iOS.csproj @@ -10,25 +10,6 @@ osu.iOS - - - - - - - - - - - - - - - - - - - @@ -38,12 +19,6 @@ - - PreserveNewest - - - PreserveNewest - @@ -79,6 +54,28 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file