diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 213c5082ab..5c11f91994 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,21 +121,12 @@ jobs: build-only-ios: name: Build only (iOS) - # change to macos-latest once GitHub finishes migrating all repositories to macOS 12. - runs-on: macos-12 + runs-on: macos-latest timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v2 - # see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617 - # remove once all workflow VMs use Xcode 14.1 - - name: Set Xcode Version - shell: bash - run: | - sudo xcode-select -s "/Applications/Xcode_14.1.app" - echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV - - name: Install .NET 6.0.x uses: actions/setup-dotnet@v1 with: diff --git a/Gemfile b/Gemfile deleted file mode 100644 index cdd3a6b349..0000000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 07ca3542f9..0000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,234 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.5) - rexml - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) - atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.653.0) - aws-sdk-core (3.166.0) - aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.59.0) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.117.1) - aws-sdk-core (~> 3, >= 3.165.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.2) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - claide (1.1.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - declarative (0.0.20) - digest-crc (0.6.4) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - emoji_regex (3.2.3) - excon (0.93.1) - faraday (1.10.2) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.0) - faraday (~> 1.0) - fastimage (2.2.6) - fastlane (2.210.1) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (~> 2.0.0) - naturally (~> 2.2) - optparse (~> 0.1.1) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) - simctl (~> 1.6.3) - 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.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-clean_testflight_testers (0.3.0) - fastlane-plugin-souyuz (0.11.1) - souyuz (= 0.11.1) - fastlane-plugin-xamarin (0.6.3) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.29.0) - google-apis-core (>= 0.9.0, < 2.a) - google-apis-core (0.9.1) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.15.0) - google-apis-core (>= 0.9.0, < 2.a) - google-apis-playcustomapp_v1 (0.12.0) - google-apis-core (>= 0.9.1, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.0) - google-cloud-storage (1.43.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.3.0) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - memoist (~> 0.16) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.5) - domain_name (~> 0.5) - httpclient (2.8.3) - jmespath (1.6.1) - json (2.6.2) - jwt (2.5.0) - memoist (0.16.2) - mini_magick (4.11.0) - mini_mime (1.1.2) - mini_portile2 (2.8.0) - multi_json (1.15.0) - multipart-post (2.0.0) - nanaimo (0.3.0) - naturally (2.2.1) - nokogiri (1.13.9) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - optparse (0.1.1) - os (1.1.4) - plist (3.6.0) - public_suffix (5.0.0) - racc (1.6.0) - rake (13.0.6) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.2.5) - rouge (2.0.7) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.3) - signet (0.17.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.8) - CFPropertyList - naturally - souyuz (0.11.1) - fastlane (>= 2.182.0) - highline (~> 2.0) - nokogiri (~> 1.7) - terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.1) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (1.8.0) - webrick (1.7.0) - word_wrap (1.0.0) - xcodeproj (1.22.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - xcpretty (0.3.0) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - ruby - -DEPENDENCIES - fastlane - fastlane-plugin-clean_testflight_testers - fastlane-plugin-souyuz - fastlane-plugin-xamarin - -BUNDLED WITH - 2.0.1 diff --git a/fastlane/Appfile b/fastlane/Appfile deleted file mode 100644 index 083de66985..0000000000 --- a/fastlane/Appfile +++ /dev/null @@ -1,2 +0,0 @@ -app_identifier("sh.ppy.osulazer") # The bundle identifier of your app -apple_id("apple-dev@ppy.sh") # Your Apple email address diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 716115e5c6..0000000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,147 +0,0 @@ -update_fastlane - -platform :android do -desc 'Deploy to play store' - lane :beta do |options| - - update_version( - version: options[:version], - build: options[:build], - ) - - build(options) - - supply( - apk: './osu.Android/bin/Release/sh.ppy.osulazer-Signed.apk', - package_name: 'sh.ppy.osulazer', - track: 'alpha', # upload to alpha, we can promote it later - json_key: options[:json_key], - ) - end - - desc 'Deploy to github release' - lane :build_github do |options| - - update_version( - version: options[:version], - build: options[:build], - ) - - build(options) - - client = HTTPClient.new - changelog = client.get_content 'https://gist.githubusercontent.com/peppy/aaa2ec1a323554b619671cac6dbbb776/raw' - changelog.gsub!('$BUILD_ID', options[:build]) - - set_github_release( - repository_name: "ppy/osu", - api_token: ENV["GITHUB_TOKEN"], - name: options[:build], - tag_name: options[:build], - is_draft: true, - description: changelog, - commitish: "master", - upload_assets: ["osu.Android/bin/Release/sh.ppy.osulazer.apk"] - ) - - end - - desc 'Compile the project' - lane :build do |options| - nuget_restore(project_path: 'osu.Android/osu.Android.csproj') - nuget_restore(project_path: 'osu.Game/osu.Game.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj') - - souyuz( - build_configuration: 'Release', - solution_path: 'osu.sln', - platform: "android", - output_path: "osu.Android/bin/Release/", - keystore_path: options[:keystore_path], - keystore_alias: options[:keystore_alias], - keystore_password: ENV["KEYSTORE_PASSWORD"] - ) - end - - lane :update_version do |options| - - split = options[:build].split('.') - split[1] = split[1].to_s.rjust(4, '0') - android_build = split.join('') - - app_version( - solution_path: 'osu.sln', - version: options[:version], - build: android_build, - ) - end - -end - -platform :ios do - desc 'Deploy to testflight' - lane :beta do |options| - update_version(options) - - provision( - type: 'appstore' - ) - - build( - build_configuration: 'Release', - build_platform: 'iPhone' - ) - - client = HTTPClient.new - changelog = client.get_content 'https://gist.githubusercontent.com/peppy/ab89c29dcc0dce95f39eb218e8fad197/raw' - changelog.gsub!('$BUILD_ID', options[:build]) - - pilot( - wait_processing_interval: 900, - changelog: changelog, - groups: ['osu! supporters', 'public'], - distribute_external: true, - ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa' - ) - end - - desc 'Compile the project' - lane :build do - nuget_restore(project_path: 'osu.iOS/osu.iOS.csproj') - nuget_restore(project_path: 'osu.Game/osu.Game.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj') - nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj') - - souyuz( - platform: "ios", - plist_path: "osu.iOS/Info.plist" - ) - end - - desc 'Install provisioning profiles using match' - lane :provision do |options| - if Helper.is_ci? - options[:readonly] = true - end - - 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:30, dry_run: true) - end - - lane :testflight_prune do - clean_testflight_testers(days_of_inactivity: 30) - end -end diff --git a/fastlane/Matchfile b/fastlane/Matchfile deleted file mode 100644 index 40c974b09e..0000000000 --- a/fastlane/Matchfile +++ /dev/null @@ -1 +0,0 @@ -git_url('https://github.com/peppy/apple-certificates') diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile deleted file mode 100644 index 9f4f47f213..0000000000 --- a/fastlane/Pluginfile +++ /dev/null @@ -1,7 +0,0 @@ -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! - -gem 'fastlane-plugin-clean_testflight_testers' -gem 'fastlane-plugin-souyuz' -gem 'fastlane-plugin-xamarin' diff --git a/fastlane/README.md b/fastlane/README.md deleted file mode 100644 index 9d5e11f7cb..0000000000 --- a/fastlane/README.md +++ /dev/null @@ -1,109 +0,0 @@ -fastlane documentation ----- - -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -```sh -xcode-select --install -``` - -For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) - -# Available Actions - -## Android - -### android beta - -```sh -[bundle exec] fastlane android beta -``` - -Deploy to play store - -### android build_github - -```sh -[bundle exec] fastlane android build_github -``` - -Deploy to github release - -### android build - -```sh -[bundle exec] fastlane android build -``` - -Compile the project - -### android update_version - -```sh -[bundle exec] fastlane android update_version -``` - - - ----- - - -## iOS - -### ios beta - -```sh -[bundle exec] fastlane ios beta -``` - -Deploy to testflight - -### ios build - -```sh -[bundle exec] fastlane ios build -``` - -Compile the project - -### ios provision - -```sh -[bundle exec] fastlane ios provision -``` - -Install provisioning profiles using match - -### ios update_version - -```sh -[bundle exec] fastlane ios update_version -``` - - - -### ios testflight_prune_dry - -```sh -[bundle exec] fastlane ios testflight_prune_dry -``` - - - -### ios testflight_prune - -```sh -[bundle exec] fastlane ios testflight_prune -``` - - - ----- - -This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. - -More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). - -The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/osu.Android.props b/osu.Android.props index c6cf7812d1..4bdf5d68c5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> </PropertyGroup> <ItemGroup> - <PackageReference Include="ppy.osu.Framework.Android" Version="2022.1226.0" /> + <PackageReference Include="ppy.osu.Framework.Android" Version="2023.131.0" /> </ItemGroup> <PropertyGroup> <!-- Fody does not handle Android build well, and warns when unchanged. diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index e0f7820262..8a0b8250d5 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()), new CatchModHidden(), new CatchModFlashlight(), + new ModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestScenePlacementBeforeTrackStart.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestScenePlacementBeforeTrackStart.cs new file mode 100644 index 0000000000..00dd75ceee --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestScenePlacementBeforeTrackStart.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestScenePlacementBeforeTrackStart : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + + [Test] + public void TestPlacement() + { + AddStep("Seek to 0", () => EditorClock.Seek(0)); + AddStep("Select note", () => InputManager.Key(Key.Number2)); + AddStep("Hover negative span", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]); + }); + AddStep("Click", () => InputManager.Click(MouseButton.Left)); + AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0)); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs new file mode 100644 index 0000000000..2c8c151e7f --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModFadeIn : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [TestCase(0.5f)] + [TestCase(0.1f)] + [TestCase(0.7f)] + public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModFadeIn { Coverage = { Value = coverage } }, PassCondition = () => true }); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs new file mode 100644 index 0000000000..204f26f151 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModHidden : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [TestCase(0.5f)] + [TestCase(0.2f)] + [TestCase(0.8f)] + public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModHidden { Coverage = { Value = coverage } }, PassCondition = () => true }); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/LongNoteTailWang.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/LongNoteTailWang.png new file mode 100644 index 0000000000..982cc1d259 Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/LongNoteTailWang.png differ diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index 9c987efc60..7c51036d69 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -14,4 +14,6 @@ Hit200: mania/hit200@2x Hit300: mania/hit300@2x Hit300g: mania/hit300g@2x StageLeft: mania/stage-left -StageRight: mania/stage-right \ No newline at end of file +StageRight: mania/stage-right +NoteImage0L: LongNoteTailWang +NoteImage1L: LongNoteTailWang diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index d367c82ed8..99a80ef28d 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Configuration.Tracking; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Mania.UI; @@ -30,8 +31,8 @@ namespace osu.Game.Rulesets.Mania.Configuration new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime, scrollTime => new SettingDescription( rawValue: scrollTime, - name: "Scroll Speed", - value: $"{scrollTime}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)})" + name: RulesetSettingsStrings.ScrollSpeed, + value: RulesetSettingsStrings.ScrollSpeedTooltip(scrollTime, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)) ) ) }; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0cec0ee075..d324682989 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()), new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new ManiaModFlashlight(), + new ModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 9ed555da51..f5a5771386 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; @@ -30,18 +31,18 @@ namespace osu.Game.Rulesets.Mania { new SettingsEnumDropdown<ManiaScrollingDirection> { - LabelText = "Scrolling direction", + LabelText = RulesetSettingsStrings.ScrollingDirection, Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) }, new SettingsSlider<double, ManiaScrollSlider> { - LabelText = "Scroll speed", + LabelText = RulesetSettingsStrings.ScrollSpeed, Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime), KeyboardStep = 5 }, new SettingsCheckbox { - LabelText = "Timing-based note colouring", + LabelText = RulesetSettingsStrings.TimingBasedColouring, Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring), } }; @@ -49,7 +50,7 @@ namespace osu.Game.Rulesets.Mania private partial class ManiaScrollSlider : OsuSliderBar<double> { - public override LocalisableString TooltipText => $"{Current.Value}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value)})"; + public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value)); } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index c6e9c339f4..196514c7b1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; @@ -18,5 +19,13 @@ namespace osu.Game.Rulesets.Mania.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; + + public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f) + { + Precision = 0.1f, + MinValue = 0.1f, + MaxValue = 0.7f, + Default = 0.5f, + }; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index eeb6e94fc7..f23cb335a5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; +using osu.Framework.Bindables; namespace osu.Game.Rulesets.Mania.Mods { @@ -13,6 +14,14 @@ namespace osu.Game.Rulesets.Mania.Mods public override LocalisableString Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; + public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f) + { + Precision = 0.1f, + MinValue = 0.2f, + MaxValue = 0.8f, + Default = 0.5f, + }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs index 6a94e5d371..09abe8d7f4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs @@ -3,8 +3,10 @@ using System; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Mods /// </summary> protected abstract CoverExpandDirection ExpandDirection { get; } + [SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")] + public abstract BindableNumber<float> Coverage { get; } + public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset) { ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; @@ -36,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Mods { c.RelativeSizeAxes = Axes.Both; c.Direction = ExpandDirection; - c.Coverage = 0.5f; + c.Coverage = Coverage.Value; })); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 759d2346e8..264a1bd5ec 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void OnFree() { - slidingSample.Samples = null; + slidingSample.ClearSamples(); base.OnFree(); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs index a2166a6708..428439d52c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs @@ -5,11 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); - private readonly Box colouredBox; - private readonly Box shadow; + private readonly Box shadeBackground; + private readonly Box shadeForeground; public ArgonHoldNoteTailPiece() { @@ -32,32 +32,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon InternalChildren = new Drawable[] { - shadow = new Box + shadeBackground = new Box { RelativeSizeAxes = Axes.Both, }, new Container { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - Height = 0.82f, - Masking = true, + Height = ArgonNotePiece.NOTE_ACCENT_RATIO, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, CornerRadius = ArgonNotePiece.CORNER_RADIUS, + Masking = true, Children = new Drawable[] { - colouredBox = new Box + shadeForeground = new Box { RelativeSizeAxes = Axes.Both, - } - } - }, - new Circle - { - RelativeSizeAxes = Axes.X, - Height = ArgonNotePiece.CORNER_RADIUS * 2, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, + }, + }, }, }; } @@ -77,19 +70,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction) { - colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up - ? Anchor.TopCentre - : Anchor.BottomCentre; + Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1); } private void onAccentChanged(ValueChangedEvent<Color4> accent) { - colouredBox.Colour = ColourInfo.GradientVertical( - accent.NewValue, - accent.NewValue.Darken(0.1f) - ); - - shadow.Colour = accent.NewValue.Darken(0.5f); + shadeBackground.Colour = accent.NewValue.Darken(1.7f); + shadeForeground.Colour = accent.NewValue.Darken(1.1f); } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs index 25b1965c18..2a5bce255c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon internal partial class ArgonNotePiece : CompositeDrawable { public const float NOTE_HEIGHT = 42; - + public const float NOTE_ACCENT_RATIO = 0.82f; public const float CORNER_RADIUS = 3.4f; private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - Height = 0.82f, + Height = NOTE_ACCENT_RATIO, Masking = true, CornerRadius = CORNER_RADIUS, Children = new Drawable[] @@ -95,6 +95,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + + Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1); } private void onAccentChanged(ValueChangedEvent<Color4> accent) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index c928ebb3e0..05ba2b8f22 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -54,6 +54,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value ?? 1; + float minimumColumnWidth = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.MinimumColumnWidth)?.Value + ?? 1; + // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. // This animation is discarded and re-queried with the appropriate frame length afterwards. var tmp = skin.GetAnimation(lightImage, true, false); @@ -92,7 +95,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy d.RelativeSizeAxes = Axes.Both; d.Size = Vector2.One; d.FillMode = FillMode.Stretch; - // Todo: Wrap + d.Height = minimumColumnWidth / d.DrawWidth * 1.6f; // constant matching stable. + // Todo: Wrap? }); if (bodySprite != null) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs index 44fe4b1b30..4a8843c999 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaScrollingDirection.cs @@ -1,15 +1,18 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { public enum ManiaScrollingDirection { + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionUp))] Up = ScrollingDirection.Up, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionDown))] Down = ScrollingDirection.Down } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index d1a04e28e5..37561fda85 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public partial class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene { private Slider slider; - private PathControlPointVisualiser visualiser; + private PathControlPointVisualiser<Slider> visualiser; [SetUp] public void Setup() => Schedule(() => @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPathType(3, null); } - private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 112aab884b..db9eea4127 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } private void assertSelectionCount(int count) => - AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == count); + AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == count); private void assertSelected(int index) => AddAssert($"{(index + 1).ToOrdinalWords()} control point piece selected", - () => this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value); + () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value); private void moveMouseToRelativePosition(Vector2 relativePosition) => AddStep($"move mouse to {relativePosition}", () => @@ -202,12 +202,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor moveMouseToControlPoint(2); AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); - AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); + AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3); addMovementStep(new Vector2(450, 50)); AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); + AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3); assertControlPointPosition(2, new Vector2(450, 50)); assertControlPointType(2, PathType.PerfectCurve); @@ -236,12 +236,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor moveMouseToControlPoint(3); AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); - AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); + AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3); addMovementStep(new Vector2(550, 50)); AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3); + AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3); // note: if the head is part of the selection being moved, the entire slider is moved. // the unselected nodes will therefore change position relative to the slider head. @@ -354,7 +354,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; - public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; + public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) : base(slider) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index ad740b2977..8ed77d45d7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; - public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; + public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) : base(slider) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs index e9d50d5118..f262a4334a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestMovingUnsnappedSliderNodesSnaps() { - PathControlPointPiece sliderEnd = null; + PathControlPointPiece<Slider> sliderEnd = null; assertSliderSnapped(false); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("select slider end", () => { - sliderEnd = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last()); + sliderEnd = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last()); InputManager.MoveMouseTo(sliderEnd.ScreenSpaceDrawQuad.Centre); }); AddStep("move slider end", () => @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("move mouse to new point location", () => { - var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]); + var firstPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]); var pos = slider.Path.PositionAt(0.25d) + slider.Position; InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos)); }); @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("move mouse to second control point", () => { - var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]); + var secondPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]); InputManager.MoveMouseTo(secondPiece); }); AddStep("quick delete", () => diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index b2ac462c8f..6cb77c7b92 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor => Editor.ChildrenOfType<ComposeBlueprintContainer>().First(); private Slider? slider; - private PathControlPointVisualiser? visualiser; + private PathControlPointVisualiser<Slider>? visualiser; private const double split_gap = 100; @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select added slider", () => { EditorBeatmap.SelectedHitObjects.Add(slider); - visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First(); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First(); }); moveMouseToControlPoint(2); @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select added slider", () => { EditorBeatmap.SelectedHitObjects.Add(slider); - visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First(); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First(); }); moveMouseToControlPoint(2); @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select added slider", () => { EditorBeatmap.SelectedHitObjects.Add(slider); - visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First(); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First(); }); moveMouseToControlPoint(2); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index a418df605f..50f9c5e775 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -5,9 +5,11 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; + [Resolved] + private OsuConfigManager config { get; set; } + [Test] public void TestHits() { @@ -56,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150))); } + [Test] + public void TestHitLighting() + { + AddToggleStep("toggle hit lighting", v => config.SetValue(OsuSetting.HitLighting, v)); + AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true))); + } + private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) { var playfield = new TestOsuPlayfield(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs index 2c9f1acd2c..718664d649 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs @@ -4,29 +4,38 @@ #nullable disable using NUnit.Framework; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public partial class TestSceneHitCircleKiai : TestSceneHitCircle + public partial class TestSceneHitCircleKiai : TestSceneHitCircle, IBeatSyncProvider { + private ControlPointInfo controlPoints { get; set; } + [SetUp] public void SetUp() => Schedule(() => { - var controlPointInfo = new ControlPointInfo(); + controlPoints = new ControlPointInfo(); - controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); - controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); + controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 }); + controlPoints.Add(0, new EffectControlPoint { KiaiMode = true }); Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - ControlPointInfo = controlPointInfo + ControlPointInfo = controlPoints }); // track needs to be playing for BeatSyncedContainer to work. Beatmap.Value.Track.Start(); }); + + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => new ChannelAmplitudes(); + ControlPointInfo IBeatSyncProvider.ControlPoints => controlPoints; + IClock IBeatSyncProvider.Clock => Clock; } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs new file mode 100644 index 0000000000..72bcec6045 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -0,0 +1,685 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +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.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public partial class TestSceneOsuTouchInput : OsuManualInputManagerTestScene + { + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + private TestActionKeyCounter leftKeyCounter = null!; + + private TestActionKeyCounter rightKeyCounter = null!; + + private OsuInputManager osuInputManager = null!; + + private Container mainContent = null!; + + [SetUpSteps] + public void SetUpSteps() + { + releaseAllTouches(); + + AddStep("Create tests", () => + { + Children = new Drawable[] + { + osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo) + { + Child = mainContent = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton) + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Depth = float.MinValue, + X = -100, + }, + rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton) + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Depth = float.MinValue, + X = 100, + }, + new OsuCursorContainer + { + Depth = float.MinValue, + } + }, + } + }, + new TouchVisualiser(), + }; + }); + } + + [Test] + public void TestStreamInputVisual() + { + addHitCircleAt(TouchSource.Touch1); + addHitCircleAt(TouchSource.Touch2); + + beginTouch(TouchSource.Touch1); + beginTouch(TouchSource.Touch2); + + endTouch(TouchSource.Touch1); + + int i = 0; + + AddRepeatStep("Alternate", () => + { + TouchSource down = i % 2 == 0 ? TouchSource.Touch3 : TouchSource.Touch4; + TouchSource up = i % 2 == 0 ? TouchSource.Touch4 : TouchSource.Touch3; + + // sometimes the user will end the previous touch before touching again, sometimes not. + if (RNG.NextBool()) + { + InputManager.BeginTouch(new Touch(down, getSanePositionForSource(down))); + InputManager.EndTouch(new Touch(up, getSanePositionForSource(up))); + } + else + { + InputManager.EndTouch(new Touch(up, getSanePositionForSource(up))); + InputManager.BeginTouch(new Touch(down, getSanePositionForSource(down))); + } + + i++; + }, 100); + } + + [Test] + public void TestSimpleInput() + { + beginTouch(TouchSource.Touch1); + + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + beginTouch(TouchSource.Touch2); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + + // Subsequent touches should be ignored (except position). + beginTouch(TouchSource.Touch3); + checkPosition(TouchSource.Touch3); + + beginTouch(TouchSource.Touch4); + checkPosition(TouchSource.Touch4); + + assertKeyCounter(1, 1); + + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + + assertKeyCounter(1, 1); + } + + [Test] + public void TestPositionalInputUpdatesOnlyFromMostRecentTouch() + { + beginTouch(TouchSource.Touch1); + checkPosition(TouchSource.Touch1); + + beginTouch(TouchSource.Touch2); + checkPosition(TouchSource.Touch2); + + beginTouch(TouchSource.Touch1, Vector2.One); + checkPosition(TouchSource.Touch2); + + endTouch(TouchSource.Touch2); + checkPosition(TouchSource.Touch2); + + // note that touch1 was never ended, but is no longer valid for touch input due to touch 2 occurring. + beginTouch(TouchSource.Touch1); + checkPosition(TouchSource.Touch2); + } + + [Test] + public void TestStreamInput() + { + // In this scenario, the user is tapping on the first object in a stream, + // then using one or two fingers in empty space to continue the stream. + + addHitCircleAt(TouchSource.Touch1); + beginTouch(TouchSource.Touch1); + + // The first touch is handled as normal. + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + // The second touch should release the first, and also act as a right button. + beginTouch(TouchSource.Touch2); + + assertKeyCounter(1, 1); + // Importantly, this is different from the simple case because an object was interacted with in the first touch, but not the second touch. + // left button is automatically released. + checkNotPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + // Also importantly, the positional part of the second touch is ignored. + checkPosition(TouchSource.Touch1); + + // In this scenario, a third touch should be allowed, and handled similarly to the second. + beginTouch(TouchSource.Touch3); + + assertKeyCounter(2, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + // Position is still ignored. + checkPosition(TouchSource.Touch1); + + endTouch(TouchSource.Touch2); + + checkPressed(OsuAction.LeftButton); + checkNotPressed(OsuAction.RightButton); + // Position is still ignored. + checkPosition(TouchSource.Touch1); + + // User continues streaming + beginTouch(TouchSource.Touch2); + + assertKeyCounter(2, 2); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + // Position is still ignored. + checkPosition(TouchSource.Touch1); + + // In this mode a maximum of three touches should be supported. + // A fourth touch should result in no changes anywhere. + beginTouch(TouchSource.Touch4); + assertKeyCounter(2, 2); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch1); + endTouch(TouchSource.Touch4); + } + + [Test] + public void TestStreamInputWithInitialTouchDownLeft() + { + // In this scenario, the user is wanting to use stream input but we start with one finger still on the screen. + // That finger is mapped to a left action. + + addHitCircleAt(TouchSource.Touch2); + + beginTouch(TouchSource.Touch1); + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + // hits circle as right action + beginTouch(TouchSource.Touch2); + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + + endTouch(TouchSource.Touch1); + checkNotPressed(OsuAction.LeftButton); + + // stream using other two fingers while touch2 tracks + beginTouch(TouchSource.Touch1); + assertKeyCounter(2, 1); + checkPressed(OsuAction.LeftButton); + // right button is automatically released + checkNotPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + + beginTouch(TouchSource.Touch3); + assertKeyCounter(2, 2); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + + endTouch(TouchSource.Touch1); + checkNotPressed(OsuAction.LeftButton); + + beginTouch(TouchSource.Touch1); + assertKeyCounter(3, 2); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + } + + [Test] + public void TestStreamInputWithInitialTouchDownRight() + { + // In this scenario, the user is wanting to use stream input but we start with one finger still on the screen. + // That finger is mapped to a right action. + + beginTouch(TouchSource.Touch1); + beginTouch(TouchSource.Touch2); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + + endTouch(TouchSource.Touch1); + + addHitCircleAt(TouchSource.Touch1); + + // hits circle as left action + beginTouch(TouchSource.Touch1); + assertKeyCounter(2, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch1); + + endTouch(TouchSource.Touch2); + + // stream using other two fingers while touch1 tracks + beginTouch(TouchSource.Touch2); + assertKeyCounter(2, 2); + checkPressed(OsuAction.RightButton); + // left button is automatically released + checkNotPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + beginTouch(TouchSource.Touch3); + assertKeyCounter(3, 2); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch1); + + endTouch(TouchSource.Touch2); + checkNotPressed(OsuAction.RightButton); + + beginTouch(TouchSource.Touch2); + assertKeyCounter(3, 3); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch1); + } + + [Test] + public void TestNonStreamOverlappingDirectTouchesWithRelease() + { + // In this scenario, the user is tapping on three circles directly while correctly releasing the first touch. + // All three should be recognised. + + addHitCircleAt(TouchSource.Touch1); + addHitCircleAt(TouchSource.Touch2); + addHitCircleAt(TouchSource.Touch3); + + beginTouch(TouchSource.Touch1); + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + beginTouch(TouchSource.Touch2); + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + + endTouch(TouchSource.Touch1); + + beginTouch(TouchSource.Touch3); + assertKeyCounter(2, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch3); + } + + [Test] + public void TestNonStreamOverlappingDirectTouchesWithoutRelease() + { + // In this scenario, the user is tapping on three circles directly without releasing any touches. + // The first two should be recognised, but a third should not (as the user already has two fingers down). + + addHitCircleAt(TouchSource.Touch1); + addHitCircleAt(TouchSource.Touch2); + addHitCircleAt(TouchSource.Touch3); + + beginTouch(TouchSource.Touch1); + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + beginTouch(TouchSource.Touch2); + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + + beginTouch(TouchSource.Touch3); + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch3); + } + + [Test] + public void TestMovementWhileDisallowed() + { + // aka "autopilot" mod + + AddStep("Disallow gameplay cursor movement", () => osuInputManager.AllowUserCursorMovement = false); + + Vector2? positionBefore = null; + + AddStep("Store cursor position", () => positionBefore = osuInputManager.CurrentState.Mouse.Position); + beginTouch(TouchSource.Touch1); + + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + AddAssert("Cursor position unchanged", () => osuInputManager.CurrentState.Mouse.Position, () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestActionWhileDisallowed() + { + // aka "relax" mod + + AddStep("Disallow gameplay actions", () => osuInputManager.AllowGameplayInputs = false); + + beginTouch(TouchSource.Touch1); + + assertKeyCounter(0, 0); + checkNotPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + } + + [Test] + public void TestInputWhileMouseButtonsDisabled() + { + AddStep("Disable mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, true)); + + beginTouch(TouchSource.Touch1); + + assertKeyCounter(0, 0); + checkNotPressed(OsuAction.LeftButton); + checkPosition(TouchSource.Touch1); + + beginTouch(TouchSource.Touch2); + + assertKeyCounter(0, 0); + checkNotPressed(OsuAction.LeftButton); + checkNotPressed(OsuAction.RightButton); + checkPosition(TouchSource.Touch2); + } + + [Test] + public void TestAlternatingInput() + { + beginTouch(TouchSource.Touch1); + + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + + beginTouch(TouchSource.Touch2); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + + for (int i = 0; i < 2; i++) + { + endTouch(TouchSource.Touch1); + + checkPressed(OsuAction.RightButton); + checkNotPressed(OsuAction.LeftButton); + + beginTouch(TouchSource.Touch1); + + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + + endTouch(TouchSource.Touch2); + + checkPressed(OsuAction.LeftButton); + checkNotPressed(OsuAction.RightButton); + + beginTouch(TouchSource.Touch2); + + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + } + } + + [Test] + public void TestPressReleaseOrder() + { + beginTouch(TouchSource.Touch1); + beginTouch(TouchSource.Touch2); + beginTouch(TouchSource.Touch3); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + + // Touch 3 was ignored, but let's ensure that if 1 or 2 are released, 3 will be handled a second attempt. + endTouch(TouchSource.Touch1); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.RightButton); + + endTouch(TouchSource.Touch3); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.RightButton); + + beginTouch(TouchSource.Touch3); + + assertKeyCounter(2, 1); + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + } + + [Test] + public void TestWithDisallowedUserCursor() + { + beginTouch(TouchSource.Touch1); + + assertKeyCounter(1, 0); + checkPressed(OsuAction.LeftButton); + + beginTouch(TouchSource.Touch2); + + assertKeyCounter(1, 1); + checkPressed(OsuAction.RightButton); + + // Subsequent touches should be ignored. + beginTouch(TouchSource.Touch3); + beginTouch(TouchSource.Touch4); + + assertKeyCounter(1, 1); + + checkPressed(OsuAction.LeftButton); + checkPressed(OsuAction.RightButton); + + assertKeyCounter(1, 1); + } + + private void addHitCircleAt(TouchSource source) + { + AddStep($"Add circle at {source}", () => + { + var hitCircle = new HitCircle(); + + hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + mainContent.Add(new DrawableHitCircle(hitCircle) + { + Clock = new FramedClock(new ManualClock()), + Position = mainContent.ToLocalSpace(getSanePositionForSource(source)), + }); + }); + } + + private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) => + AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source)))); + + private void endTouch(TouchSource source, Vector2? screenSpacePosition = null) => + AddStep($"Release touch for {source}", () => InputManager.EndTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source)))); + + private Vector2 getSanePositionForSource(TouchSource source) + { + return new Vector2( + osuInputManager.ScreenSpaceDrawQuad.Centre.X + osuInputManager.ScreenSpaceDrawQuad.Width * (-1 + (int)source) / 8, + osuInputManager.ScreenSpaceDrawQuad.Centre.Y - 100 + ); + } + + private void checkPosition(TouchSource touchSource) => + AddAssert("Cursor position is correct", () => osuInputManager.CurrentState.Mouse.Position, () => Is.EqualTo(getSanePositionForSource(touchSource))); + + private void assertKeyCounter(int left, int right) + { + AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left)); + AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right)); + } + + private void releaseAllTouches() + { + AddStep("Release all touches", () => + { + config.SetValue(OsuSetting.MouseDisableButtons, false); + foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources) + InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre)); + }); + } + + private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action)); + private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action)); + + public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler<OsuAction> + { + public OsuAction Action { get; } + + public TestActionKeyCounter(OsuAction action) + : base(action.ToString()) + { + Action = action; + } + + public bool OnPressed(KeyBindingPressEvent<OsuAction> e) + { + if (e.Action == Action) + { + IsLit = true; + Increment(); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent<OsuAction> e) + { + if (e.Action == Action) IsLit = false; + } + } + + public partial class TouchVisualiser : CompositeDrawable + { + private readonly Drawable?[] drawableTouches = new Drawable?[TouchState.MAX_TOUCH_COUNT]; + + public TouchVisualiser() + { + RelativeSizeAxes = Axes.Both; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override bool OnTouchDown(TouchDownEvent e) + { + if (IsDisposed) + return false; + + var circle = new Circle + { + Alpha = 0.5f, + Origin = Anchor.Centre, + Size = new Vector2(20), + Position = e.Touch.Position, + Colour = colourFor(e.Touch.Source), + }; + + AddInternal(circle); + drawableTouches[(int)e.Touch.Source] = circle; + return false; + } + + protected override void OnTouchMove(TouchMoveEvent e) + { + if (IsDisposed) + return; + + var circle = drawableTouches[(int)e.Touch.Source]; + + Debug.Assert(circle != null); + + AddInternal(new FadingCircle(circle)); + circle.Position = e.Touch.Position; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + var circle = drawableTouches[(int)e.Touch.Source]; + + Debug.Assert(circle != null); + + circle.FadeOut(200, Easing.OutQuint).Expire(); + drawableTouches[(int)e.Touch.Source] = null; + } + + private Color4 colourFor(TouchSource source) + { + return Color4.FromHsv(new Vector4((float)source / TouchState.MAX_TOUCH_COUNT, 1f, 1f, 1f)); + } + + private partial class FadingCircle : Circle + { + public FadingCircle(Drawable source) + { + Origin = Anchor.Centre; + Size = source.Size; + Position = source.Position; + Colour = source.Colour; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeOut(200).Expire(); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs index 0af0ff5604..a32f0a13b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs @@ -31,10 +31,8 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestMaximumDistanceTrackingWithoutMovement( - [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - float circleSize, - [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] - double velocity) + [Values(0, 5, 10)] float circleSize, + [Values(0, 5, 10)] double velocity) { const double time_slider_start = 1000; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 28e0d650c4..67685d21a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -8,34 +8,36 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { /// <summary> - /// A visualisation of the line between two <see cref="PathControlPointPiece"/>s. + /// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s. /// </summary> - public partial class PathControlPointConnectionPiece : CompositeDrawable + /// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam> + public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath { public readonly PathControlPoint ControlPoint; private readonly Path path; - private readonly Slider slider; + private readonly T hitObject; public int ControlPointIndex { get; set; } - private IBindable<Vector2> sliderPosition; + private IBindable<Vector2> hitObjectPosition; private IBindable<int> pathVersion; - public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) + public PathControlPointConnectionPiece(T hitObject, int controlPointIndex) { - this.slider = slider; + this.hitObject = hitObject; ControlPointIndex = controlPointIndex; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; - ControlPoint = slider.Path.ControlPoints[controlPointIndex]; + ControlPoint = hitObject.Path.ControlPoints[controlPointIndex]; InternalChild = path = new SmoothPath { @@ -48,10 +50,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { base.LoadComplete(); - sliderPosition = slider.PositionBindable.GetBoundCopy(); - sliderPosition.BindValueChanged(_ => updateConnectingPath()); + hitObjectPosition = hitObject.PositionBindable.GetBoundCopy(); + hitObjectPosition.BindValueChanged(_ => updateConnectingPath()); - pathVersion = slider.Path.Version.GetBoundCopy(); + pathVersion = hitObject.Path.Version.GetBoundCopy(); pathVersion.BindValueChanged(_ => updateConnectingPath()); updateConnectingPath(); @@ -62,16 +64,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// </summary> private void updateConnectingPath() { - Position = slider.StackedPosition + ControlPoint.Position; + Position = hitObject.StackedPosition + ControlPoint.Position; path.ClearVertices(); int nextIndex = ControlPointIndex + 1; - if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) + if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count) return; path.AddVertex(Vector2.Zero); - path.AddVertex(slider.Path.ControlPoints[nextIndex].Position - ControlPoint.Position); + path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index d83f35d13f..12e5ca0236 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -29,11 +29,13 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { /// <summary> - /// A visualisation of a single <see cref="PathControlPoint"/> in a <see cref="Slider"/>. + /// A visualisation of a single <see cref="PathControlPoint"/> in an osu hit object with a path. /// </summary> - public partial class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip + /// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointPiece{T}"/> visualises.</typeparam> + public partial class PathControlPointPiece<T> : BlueprintPiece<T>, IHasTooltip + where T : OsuHitObject, IHasPath { - public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; + public Action<PathControlPointPiece<T>, MouseButtonEvent> RequestSelection; public Action<PathControlPoint> DragStarted; public Action<DragEvent> DragInProgress; @@ -44,34 +46,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public readonly BindableBool IsSelected = new BindableBool(); public readonly PathControlPoint ControlPoint; - private readonly Slider slider; + private readonly T hitObject; private readonly Container marker; private readonly Drawable markerRing; [Resolved] private OsuColour colours { get; set; } - private IBindable<Vector2> sliderPosition; - private IBindable<float> sliderScale; + private IBindable<Vector2> hitObjectPosition; + private IBindable<float> hitObjectScale; [UsedImplicitly] - private readonly IBindable<int> sliderVersion; + private readonly IBindable<int> hitObjectVersion; - public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) + public PathControlPointPiece(T hitObject, PathControlPoint controlPoint) { - this.slider = slider; + this.hitObject = hitObject; ControlPoint = controlPoint; - // we don't want to run the path type update on construction as it may inadvertently change the slider. - cachePoints(slider); + // we don't want to run the path type update on construction as it may inadvertently change the hit object. + cachePoints(hitObject); - sliderVersion = slider.Path.Version.GetBoundCopy(); + hitObjectVersion = hitObject.Path.Version.GetBoundCopy(); // schedule ensure that updates are only applied after all operations from a single frame are applied. - // this avoids inadvertently changing the slider path type for batch operations. - sliderVersion.BindValueChanged(_ => Scheduler.AddOnce(() => + // this avoids inadvertently changing the hit object path type for batch operations. + hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() => { - cachePoints(slider); + cachePoints(hitObject); updatePathType(); })); @@ -120,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { base.LoadComplete(); - sliderPosition = slider.PositionBindable.GetBoundCopy(); - sliderPosition.BindValueChanged(_ => updateMarkerDisplay()); + hitObjectPosition = hitObject.PositionBindable.GetBoundCopy(); + hitObjectPosition.BindValueChanged(_ => updateMarkerDisplay()); - sliderScale = slider.ScaleBindable.GetBoundCopy(); - sliderScale.BindValueChanged(_ => updateMarkerDisplay()); + hitObjectScale = hitObject.ScaleBindable.GetBoundCopy(); + hitObjectScale.BindValueChanged(_ => updateMarkerDisplay()); IsSelected.BindValueChanged(_ => updateMarkerDisplay()); @@ -212,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke(); - private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint); + private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint); /// <summary> /// Handles correction of invalid path types. @@ -239,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// </summary> private void updateMarkerDisplay() { - Position = slider.StackedPosition + ControlPoint.Position; + Position = hitObject.StackedPosition + ControlPoint.Position; markerRing.Alpha = IsSelected.Value ? 1 : 0; @@ -249,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components colour = colour.Lighten(1); marker.Colour = colour; - marker.Scale = new Vector2(slider.Scale); + marker.Scale = new Vector2(hitObject.Scale); } private Color4 getColourFromNodeType() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 66d99e0bf1..17d0fc457a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -29,15 +29,16 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { - public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu + public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu + where T : OsuHitObject, IHasPath { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. - internal readonly Container<PathControlPointPiece> Pieces; - internal readonly Container<PathControlPointConnectionPiece> Connections; + internal readonly Container<PathControlPointPiece<T>> Pieces; + internal readonly Container<PathControlPointConnectionPiece<T>> Connections; private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>(); - private readonly Slider slider; + private readonly T hitObject; private readonly bool allowSelection; private InputManager inputManager; @@ -48,17 +49,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } - public PathControlPointVisualiser(Slider slider, bool allowSelection) + public PathControlPointVisualiser(T hitObject, bool allowSelection) { - this.slider = slider; + this.hitObject = hitObject; this.allowSelection = allowSelection; RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both }, - Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both } + Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both }, + Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both } }; } @@ -69,12 +70,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components inputManager = GetContainingInputManager(); controlPoints.CollectionChanged += onControlPointsChanged; - controlPoints.BindTo(slider.Path.ControlPoints); + controlPoints.BindTo(hitObject.Path.ControlPoints); } /// <summary> - /// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>, - /// and deselects all other <see cref="PathControlPointPiece"/>s. + /// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>, + /// and deselects all other <see cref="PathControlPointPiece{T}"/>s. /// </summary> public void SetSelectionTo(PathControlPoint pathControlPoint) { @@ -124,8 +125,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } - private bool isSplittable(PathControlPointPiece p) => - // A slider can only be split on control points which connect two different slider segments. + private bool isSplittable(PathControlPointPiece<T> p) => + // A hit object can only be split on control points which connect two different path segments. p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault(); private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -150,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { var point = (PathControlPoint)e.NewItems[i]; - Pieces.Add(new PathControlPointPiece(slider, point).With(d => + Pieces.Add(new PathControlPointPiece<T>(hitObject, point).With(d => { if (allowSelection) d.RequestSelection = selectionRequested; @@ -160,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components d.DragEnded = dragEnded; })); - Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); + Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i)); } break; @@ -219,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } - private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e) + private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) piece.IsSelected.Toggle(); @@ -234,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// </summary> /// <param name="piece">The control point piece that we want to change the path type of.</param> /// <param name="type">The path type we want to assign to the given control point piece.</param> - private void updatePathType(PathControlPointPiece piece, PathType? type) + private void updatePathType(PathControlPointPiece<T> piece, PathType? type) { int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); @@ -252,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components break; } - slider.Path.ExpectedDistance.Value = null; + hitObject.Path.ExpectedDistance.Value = null; piece.ControlPoint.Type = type; } @@ -268,9 +269,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void dragStarted(PathControlPoint controlPoint) { - dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray(); - dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray(); - draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint); + dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); + dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray(); + draggedControlPointIndex = hitObject.Path.ControlPoints.IndexOf(controlPoint); selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint)); Debug.Assert(draggedControlPointIndex >= 0); @@ -280,25 +281,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void dragInProgress(DragEvent e) { - Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray(); - var oldPosition = slider.Position; - double oldStartTime = slider.StartTime; + Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray(); + var oldPosition = hitObject.Position; + double oldStartTime = hitObject.StartTime; - if (selectedControlPoints.Contains(slider.Path.ControlPoints[0])) + if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0])) { - // Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account + // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition); - Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position; + Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; - slider.Position += movementDelta; - slider.StartTime = result?.Time ?? slider.StartTime; + hitObject.Position += movementDelta; + hitObject.StartTime = result?.Time ?? hitObject.StartTime; - for (int i = 1; i < slider.Path.ControlPoints.Count; i++) + for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { - var controlPoint = slider.Path.ControlPoints[i]; - // Since control points are relative to the position of the slider, all points that are _not_ selected + var controlPoint = hitObject.Path.ControlPoints[i]; + // Since control points are relative to the position of the hit object, all points that are _not_ selected // need to be offset _back_ by the delta corresponding to the movement of the head point. // All other selected control points (if any) will move together with the head point // (and so they will not move at all, relative to each other). @@ -310,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition)); - Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - slider.Position; + Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { @@ -321,23 +322,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } // Snap the path to the current beat divisor before checking length validity. - slider.SnapTo(snapProvider); + hitObject.SnapTo(snapProvider); - if (!slider.Path.HasValidLength) + if (!hitObject.Path.HasValidLength) { - for (int i = 0; i < slider.Path.ControlPoints.Count; i++) - slider.Path.ControlPoints[i].Position = oldControlPoints[i]; + for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) + hitObject.Path.ControlPoints[i].Position = oldControlPoints[i]; - slider.Position = oldPosition; - slider.StartTime = oldStartTime; + hitObject.Position = oldPosition; + hitObject.StartTime = oldStartTime; // Snap the path length again to undo the invalid length. - slider.SnapTo(snapProvider); + hitObject.SnapTo(snapProvider); return; } // Maintain the path types in case they got defaulted to bezier at some point during the drag. - for (int i = 0; i < slider.Path.ControlPoints.Count; i++) - slider.Path.ControlPoints[i].Type = dragPathTypes[i]; + for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) + hitObject.Path.ControlPoints[i].Type = dragPathTypes[i]; } private void dragEnded() => changeHandler?.EndChange(); 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 ecd840dda6..68a44eb2f8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// </summary> public Vector2 PathStartLocation => body.PathOffset; + /// <summary> + /// Offset in absolute (local) coordinates from the end of the curve. + /// </summary> + public Vector2 PathEndLocation => body.PathEndOffset; + public SliderBodyPiece() { InternalChild = body = new ManualSliderBody diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f91d35e2e1..77393efeb3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private SliderBodyPiece bodyPiece; private HitCirclePiece headCirclePiece; private HitCirclePiece tailCirclePiece; - private PathControlPointVisualiser controlPointVisualiser; + private PathControlPointVisualiser<Slider> controlPointVisualiser; private InputManager inputManager; @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders bodyPiece = new SliderBodyPiece(), headCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(), - controlPointVisualiser = new PathControlPointVisualiser(HitObject, false) + controlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, false) }; setState(SliderPlacementState.Initial); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index a51c223785..e444287b73 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected SliderCircleOverlay TailOverlay { get; private set; } [CanBeNull] - protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } + protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; } [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { if (ControlPointVisualiser == null) { - AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true) + AddInternal(ControlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, true) { RemoveControlPointsRequested = removeControlPoints, SplitControlPointsRequested = splitControlPoints @@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); + protected override Vector2[] ScreenSpaceAdditionalNodes => new[] + { + DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation) + }; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs new file mode 100644 index 0000000000..5b79753632 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModAccuracyChallenge : ModAccuracyChallenge + { + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 4601af45d8..a7b02596d5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables PathVersion.UnbindFrom(HitObject.Path.Version); - slidingSample.Samples = null; + slidingSample?.ClearSamples(); } protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index eed5c3e2e3..a5193f1b6e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnFree(); - spinningSample.Samples = null; + spinningSample.ClearSamples(); } protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index 7dede9e283..ccd388192e 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -1,17 +1,18 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using osu.Framework.Input; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Input.StateChanges.Events; using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Osu { @@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu /// </remarks> public bool AllowGameplayInputs { + get => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs; set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value; } @@ -40,11 +42,24 @@ namespace osu.Game.Rulesets.Osu protected override KeyBindingContainer<OsuAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new OsuKeyBindingContainer(ruleset, variant, unique); + public bool CheckScreenSpaceActionPressJudgeable(Vector2 screenSpacePosition) => + // This is a very naive but simple approach. + // + // Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected), + // this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can. + NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition)); + public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) { } + [BackgroundDependencyLoader] + private void load() + { + Add(new OsuTouchInputMapper(this) { RelativeSizeAxes = Axes.Both }); + } + protected override bool Handle(UIEvent e) { if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; @@ -52,19 +67,6 @@ namespace osu.Game.Rulesets.Osu return base.Handle(e); } - protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e) - { - if (!AllowUserCursorMovement) - { - // Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse. - // Primarily relied upon by the "autopilot" osu! mod. - var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position); - e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null); - } - - return base.HandleMouseTouchStateChange(e); - } - private partial class OsuKeyBindingContainer : RulesetKeyBindingContainer { private bool allowGameplayInputs = true; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 79a566e33c..48056a49de 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new OsuModHidden(), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), - new OsuModStrictTracking() + new OsuModStrictTracking(), + new OsuModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs index db458ec48a..1c5cf49625 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; @@ -43,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private readonly IBindable<Color4> accentColour = new Bindable<Color4>(); private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>(); private readonly FlashPiece flash; + private readonly Container kiaiContainer; + + private Bindable<bool> configHitLighting = null!; [Resolved] private DrawableHitObject drawableObject { get; set; } = null!; @@ -64,24 +68,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon outerGradient = new Circle // renders the outer bright gradient { Size = new Vector2(OUTER_GRADIENT_SIZE), - Alpha = 1, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, innerGradient = new Circle // renders the inner bright gradient { Size = new Vector2(INNER_GRADIENT_SIZE), - Alpha = 1, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, innerFill = new Circle // renders the inner dark fill { Size = new Vector2(INNER_FILL_SIZE), - Alpha = 1, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, + kiaiContainer = new CircularContainer + { + Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = Size, + Child = new KiaiFlash + { + RelativeSizeAxes = Axes.Both, + } + }, number = new OsuSpriteText { Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold), @@ -96,12 +108,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { var drawableOsuObject = (DrawableOsuHitObject)drawableObject; accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); + + configHitLighting = config.GetBindable<bool>(OsuSetting.HitLighting); } protected override void LoadComplete() @@ -117,20 +131,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon outerGradient.ClearTransforms(targetMember: nameof(Colour)); outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f)); + kiaiContainer.Colour = colour.NewValue; outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4); innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f)); flash.Colour = colour.NewValue; // Accent colour may be changed many times during a paused gameplay state. // Schedule the change to avoid transforms piling up. - Scheduler.AddOnce(updateStateTransforms); + Scheduler.AddOnce(() => + { + ApplyTransformsAt(double.MinValue, true); + ClearTransformsAfter(double.MinValue, true); + + updateStateTransforms(drawableObject, drawableObject.State.Value); + }); }, true); drawableObject.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value); - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) @@ -140,7 +159,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon case ArmedState.Hit: // Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec. const double fade_out_time = 800; - const double flash_in_duration = 150; const double resize_duration = 400; @@ -171,20 +189,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon // gradient layers. border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf); + // Kiai flash should track the overall size but also be cleaned up quite fast, so we don't get additional + // flashes after the hit animation is already in a mostly-completed state. + kiaiContainer.ResizeTo(Size * shrink_size, resize_duration, Easing.OutElasticHalf); + kiaiContainer.FadeOut(flash_in_duration, Easing.OutQuint); + // The outer gradient is resize with a slight delay from the border. // This is to give it a bomb-like effect, with the border "triggering" its animation when getting close. using (BeginDelayedSequence(flash_in_duration / 12)) { outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf); + outerGradient .FadeColour(Color4.White, 80) .Then() .FadeOut(flash_in_duration); } - flash.FadeTo(1, flash_in_duration, Easing.OutQuint); + if (configHitLighting.Value) + { + flash.HitLighting = true; + flash.FadeTo(1, flash_in_duration, Easing.OutQuint); + + this.FadeOut(fade_out_time, Easing.OutQuad); + } + else + { + flash.HitLighting = false; + flash.FadeTo(1, flash_in_duration, Easing.OutQuint) + .Then() + .FadeOut(flash_in_duration, Easing.OutQuint); + + this.FadeOut(fade_out_time * 0.8f, Easing.OutQuad); + } - this.FadeOut(fade_out_time, Easing.OutQuad); break; } } @@ -215,6 +253,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon Child.AlwaysPresent = true; } + public bool HitLighting { get; set; } + protected override void Update() { base.Update(); @@ -223,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { Type = EdgeEffectType.Glow, Colour = Colour, - Radius = OsuHitObject.OBJECT_RADIUS * 1.2f, + Radius = OsuHitObject.OBJECT_RADIUS * (HitLighting ? 1.2f : 0.6f), }; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs index 0bd5fd4cac..44962c8548 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs @@ -35,14 +35,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public void SetRotation(float currentRotation) { - // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. - if (Precision.AlmostEquals(0, Time.Elapsed)) - return; - // If we've gone back in time, it's fine to work with a fresh set of records for now if (records.Count > 0 && Time.Current < records.Last().Time) records.Clear(); + // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. + if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time)) + return; + if (records.Count > 0) { var record = records.Peek(); diff --git a/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs index 283687adfd..e7885e65de 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SliderBody.cs @@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning /// </summary> public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]); + /// <summary> + /// Offset in absolute coordinates from the end of the curve. + /// </summary> + public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]); + /// <summary> /// Used to colour the path. /// </summary> diff --git a/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs index f8ee465cd6..0b7acc1f47 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SnakingSliderBody.cs @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning public override Vector2 PathOffset => snakedPathOffset; + public override Vector2 PathEndOffset => snakedPathEndOffset; + /// <summary> /// The top-left position of the path when fully snaked. /// </summary> @@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning /// </summary> private Vector2 snakedPathOffset; + /// <summary> + /// The offset of the end of path from <see cref="snakedPosition"/> when fully snaked. + /// </summary> + private Vector2 snakedPathEndOffset; + private DrawableSlider drawableSlider = null!; [BackgroundDependencyLoader] @@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); + snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]); double lastSnakedStart = SnakedStart ?? 0; double lastSnakedEnd = SnakedEnd ?? 0; diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index f711a0fc31..64c4e7eef6 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.UI; @@ -30,23 +29,23 @@ namespace osu.Game.Rulesets.Osu.UI { new SettingsCheckbox { - LabelText = "Snaking in sliders", + LabelText = RulesetSettingsStrings.SnakingInSliders, Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders) }, new SettingsCheckbox { ClassicDefault = false, - LabelText = "Snaking out sliders", + LabelText = RulesetSettingsStrings.SnakingOutSliders, Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders) }, new SettingsCheckbox { - LabelText = "Cursor trail", + LabelText = RulesetSettingsStrings.CursorTrail, Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail) }, new SettingsEnumDropdown<PlayfieldBorderStyle> { - LabelText = "Playfield border style", + LabelText = RulesetSettingsStrings.PlayfieldBorderStyle, Current = config.GetBindable<PlayfieldBorderStyle>(OsuRulesetSetting.PlayfieldBorderStyle), }, }; diff --git a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs new file mode 100644 index 0000000000..8df1c35b5c --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs @@ -0,0 +1,161 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; +using osu.Game.Configuration; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class OsuTouchInputMapper : Drawable + { + /// <summary> + /// All the active <see cref="TouchSource"/>s and the <see cref="OsuAction"/> that it triggered (if any). + /// Ordered from oldest to newest touch chronologically. + /// </summary> + private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>(); + + private TrackedTouch? positionTrackingTouch; + + private readonly OsuInputManager osuInputManager; + + private Bindable<bool> mouseDisabled = null!; + + public OsuTouchInputMapper(OsuInputManager inputManager) + { + osuInputManager = inputManager; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + // The mouse button disable setting affects touch. It's a bit weird. + // This is mostly just doing the same as what is done in RulesetInputManager to match behaviour. + mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons); + } + + // Required to handle touches outside of the playfield when screen scaling is enabled. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + protected override void OnTouchMove(TouchMoveEvent e) + { + base.OnTouchMove(e); + handleTouchMovement(e); + } + + protected override bool OnTouchDown(TouchDownEvent e) + { + OsuAction action = trackedTouches.Any(t => t.Action == OsuAction.LeftButton) + ? OsuAction.RightButton + : OsuAction.LeftButton; + + // Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future. + bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action); + + // If we can actually accept as an action, check whether this tap was on a circle's receptor. + // This case gets special handling to allow for empty-space stream tapping. + bool isDirectCircleTouch = osuInputManager.CheckScreenSpaceActionPressJudgeable(e.ScreenSpaceTouchDownPosition); + + var newTouch = new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null, isDirectCircleTouch); + + updatePositionTracking(newTouch); + + trackedTouches.Add(newTouch); + + // Important to update position before triggering the pressed action. + handleTouchMovement(e); + + if (shouldResultInAction) + osuInputManager.KeyBindingContainer.TriggerPressed(action); + + return true; + } + + /// <summary> + /// Given a new touch, update the positional tracking state and any related operations. + /// </summary> + private void updatePositionTracking(TrackedTouch newTouch) + { + // If the new touch directly interacted with a circle's receptor, it always becomes the current touch for positional tracking. + if (newTouch.DirectTouch) + { + positionTrackingTouch = newTouch; + return; + } + + // Otherwise, we only want to use the new touch for position tracking if no other touch is tracking position yet.. + if (positionTrackingTouch == null) + { + positionTrackingTouch = newTouch; + return; + } + + // ..or if the current position tracking touch was not a direct touch (this one is debatable and may be change in the future, but it's the simplest way to handle) + if (!positionTrackingTouch.DirectTouch) + { + positionTrackingTouch = newTouch; + return; + } + + // In the case the new touch was not used for position tracking, we should also check the previous position tracking touch. + // If it was a direct touch and still has its action pressed, that action should be released. + // + // This is done to allow tracking with the initial touch while still having both Left/Right actions available for alternating with two more touches. + if (positionTrackingTouch.DirectTouch && positionTrackingTouch.Action is OsuAction directTouchAction) + { + osuInputManager.KeyBindingContainer.TriggerReleased(directTouchAction); + positionTrackingTouch.Action = null; + } + } + + private void handleTouchMovement(TouchEvent touchEvent) + { + // Movement should only be tracked for the most recent touch. + if (touchEvent.Touch.Source != positionTrackingTouch?.Source) + return; + + if (!osuInputManager.AllowUserCursorMovement) + return; + + new MousePositionAbsoluteInput { Position = touchEvent.ScreenSpaceTouch.Position }.Apply(osuInputManager.CurrentState, osuInputManager); + } + + protected override void OnTouchUp(TouchUpEvent e) + { + var tracked = trackedTouches.Single(t => t.Source == e.Touch.Source); + + if (tracked.Action is OsuAction action) + osuInputManager.KeyBindingContainer.TriggerReleased(action); + + if (positionTrackingTouch == tracked) + positionTrackingTouch = null; + + trackedTouches.Remove(tracked); + + base.OnTouchUp(e); + } + + private class TrackedTouch + { + public readonly TouchSource Source; + + public OsuAction? Action; + + public readonly bool DirectTouch; + + public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch) + { + Source = source; + Action = action; + DirectTouch = directTouch; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 3b3b3e606c..2ccdfd40e5 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; @@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield + public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject> { - private DrawableTaikoRuleset? drawableTaikoRuleset; - public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) { - drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; + var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; var playfield = (TaikoPlayfield)drawableRuleset.Playfield; playfield.ClassicHitTargetPosition.Value = true; } - - public void Update(Playfield playfield) - { - Debug.Assert(drawableTaikoRuleset != null); - - // Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. - const float scroll_rate = 10; - - // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. - float ratio = drawableTaikoRuleset.DrawHeight / 480; - - drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; - } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index fe12cf9765..1d0f772db0 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -144,6 +144,7 @@ namespace osu.Game.Rulesets.Taiko new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()), new TaikoModHidden(), new TaikoModFlashlight(), + new ModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 146daa8c27..40203440c5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Left; - TimeRange.Value = 7000; } [BackgroundDependencyLoader] @@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI KeyBindingInputManager.Add(new DrumTouchInputArea()); } + protected override void Update() + { + base.Update(); + + // Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. + const float scroll_rate = 10; + + // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. + float ratio = DrawHeight / 480; + + TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); diff --git a/osu.Game.Tests/Editing/TestSceneSnappingNearZero.cs b/osu.Game.Tests/Editing/TestSceneSnappingNearZero.cs new file mode 100644 index 0000000000..59081215ba --- /dev/null +++ b/osu.Game.Tests/Editing/TestSceneSnappingNearZero.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps.ControlPoints; + +namespace osu.Game.Tests.Editing +{ + [TestFixture] + public class TestSceneSnappingNearZero + { + private readonly ControlPointInfo cpi = new ControlPointInfo(); + + [Test] + public void TestOnZero() + { + test(0, 500, 0, 0); + test(0, 500, 100, 0); + test(0, 500, 250, 500); + test(0, 500, 600, 500); + + test(0, 500, -600, 0); + } + + [Test] + public void TestAlmostOnZero() + { + test(50, 500, 0, 50); + test(50, 500, 50, 50); + test(50, 500, 100, 50); + test(50, 500, 299, 50); + test(50, 500, 300, 550); + + test(50, 500, -500, 50); + } + + [Test] + public void TestAlmostOnOne() + { + test(499, 500, -1, 499); + test(499, 500, 0, 499); + test(499, 500, 1, 499); + test(499, 500, 499, 499); + test(499, 500, 600, 499); + test(499, 500, 800, 999); + } + + [Test] + public void TestOnOne() + { + test(500, 500, -500, 0); + test(500, 500, 0, 0); + test(500, 500, 200, 0); + test(500, 500, 400, 500); + test(500, 500, 500, 500); + test(500, 500, 600, 500); + test(500, 500, 900, 1000); + } + + [Test] + public void TestNegative() + { + test(-600, 500, -600, 400); + test(-600, 500, -100, 400); + test(-600, 500, 0, 400); + test(-600, 500, 200, 400); + test(-600, 500, 400, 400); + test(-600, 500, 600, 400); + test(-600, 500, 1000, 900); + } + + private void test(double pointTime, double beatLength, double from, double expected) + { + cpi.Clear(); + cpi.Add(pointTime, new TimingControlPoint { BeatLength = beatLength }); + Assert.That(cpi.GetClosestSnappedTime(from, 1), Is.EqualTo(expected), $"From: {from}"); + } + } +} diff --git a/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs b/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs index 6f3fe875e3..8ebf34b1ca 100644 --- a/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs +++ b/osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.NonVisual public void TestExactDivisors() { var cpi = new ControlPointInfo(); - cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 }); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 }; @@ -47,7 +47,7 @@ namespace osu.Game.Tests.NonVisual public void TestExactDivisorsHighBPMStream() { var cpi = new ControlPointInfo(); - cpi.Add(-50, new TimingControlPoint { BeatLength = 50 }); // 1200 BPM 1/4 (limit testing) + cpi.Add(0, new TimingControlPoint { BeatLength = 50 }); // 1200 BPM 1/4 (limit testing) // A 1/4 stream should land on 1/1, 1/2 and 1/4 divisors. double[] divisors = { 4, 4, 4, 4, 4, 4, 4, 4 }; @@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual public void TestApproximateDivisors() { var cpi = new ControlPointInfo(); - cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 }); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); double[] divisors = { 3.03d, 0.97d, 14, 13, 7.94d, 6.08d, 3.93d, 2.96d, 2.02d, 64 }; double[] closestDivisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 }; @@ -68,7 +68,7 @@ namespace osu.Game.Tests.NonVisual assertClosestDivisors(divisors, closestDivisors, cpi); } - private void assertClosestDivisors(IReadOnlyList<double> divisors, IReadOnlyList<double> closestDivisors, ControlPointInfo cpi, double step = 1) + private static void assertClosestDivisors(IReadOnlyList<double> divisors, IReadOnlyList<double> closestDivisors, ControlPointInfo cpi, double step = 1) { List<HitObject> hitobjects = new List<HitObject>(); double offset = cpi.TimingPoints[0].Time; diff --git a/osu.Game.Tests/Resources/Archives/conflicting-filenames-beatmap.osz b/osu.Game.Tests/Resources/Archives/conflicting-filenames-beatmap.osz new file mode 100644 index 0000000000..c6b5f083ff Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/conflicting-filenames-beatmap.osz differ diff --git a/osu.Game.Tests/Resources/Archives/conflicting-filenames-skin.osk b/osu.Game.Tests/Resources/Archives/conflicting-filenames-skin.osk new file mode 100644 index 0000000000..73576f3e22 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/conflicting-filenames-skin.osk differ diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20221024.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20221024.osk new file mode 100644 index 0000000000..28b6a001eb Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20221024.osk differ diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20230117.osk b/osu.Game.Tests/Resources/Archives/modified-default-20230117.osk new file mode 100644 index 0000000000..810bc4edf0 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-default-20230117.osk differ diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 3fba21050e..5cfcca303f 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -150,6 +150,8 @@ namespace osu.Game.Tests.Rulesets public IBindable<double> AggregateTempo => throw new NotImplementedException(); public int PlaybackConcurrency { get; set; } + + public void AddExtension(string extension) => throw new NotImplementedException(); } private class TestShaderManager : ShaderManager diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 39e8ec5215..3bab91bd9c 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -41,10 +41,14 @@ namespace osu.Game.Tests.Skins "Archives/modified-default-20220818.osk", // Covers longest combo counter "Archives/modified-default-20221012.osk", + // Covers Argon variant of song progress bar + "Archives/modified-argon-20221024.osk", // Covers TextElement and BeatmapInfoDrawable "Archives/modified-default-20221102.osk", // Covers BPM counter. - "Archives/modified-default-20221205.osk" + "Archives/modified-default-20221205.osk", + // Covers judgement counter. + "Archives/modified-default-20230117.osk" }; /// <summary> diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 5256bcb3af..d9212386c3 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -1,12 +1,11 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -20,29 +19,36 @@ namespace osu.Game.Tests.Skins public partial class TestSceneBeatmapSkinResources : OsuTestScene { [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; - private IWorkingBeatmap beatmap; - - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestRetrieveOggAudio() { - var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-beatmap.osz"), "ogg-beatmap.osz")).GetResultSafely(); + IWorkingBeatmap beatmap = null!; - imported?.PerformRead(s => + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"ogg-beatmap.osz")); + AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"sample")) != null); + AddAssert("track is non-null", () => { - beatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]); + using (var track = beatmap.LoadTrack()) + return track is not TrackVirtual; }); } [Test] - public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); - - [Test] - public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => + public void TestRetrievalWithConflictingFilenames() { - using (var track = beatmap.LoadTrack()) - return track is not TrackVirtual; - }); + IWorkingBeatmap beatmap = null!; + + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"conflicting-filenames-beatmap.osz")); + AddAssert("texture is non-null", () => beatmap.Skin.GetTexture(@"spinner-osu") != null); + AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null); + } + + private IWorkingBeatmap importBeatmapFromArchives(string filename) + { + var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); + return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0])); + } } } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 748668b6ca..aaec319b57 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -31,17 +31,24 @@ namespace osu.Game.Tests.Skins [Resolved] private SkinManager skins { get; set; } = null!; - private ISkin skin = null!; - - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestRetrieveOggSample() { - var imported = skins.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-skin.osk"), "ogg-skin.osk")).GetResultSafely(); - skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo)); + ISkin skin = null!; + + AddStep("import skin", () => skin = importSkinFromArchives(@"ogg-skin.osk")); + AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo(@"sample")) != null); } [Test] - public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null); + public void TestRetrievalWithConflictingFilenames() + { + ISkin skin = null!; + + AddStep("import skin", () => skin = importSkinFromArchives(@"conflicting-filenames-skin.osk")); + AddAssert("texture is non-null", () => skin.GetTexture(@"spinner-osu") != null); + AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo(@"spinner-osu")) != null); + } [Test] public void TestSampleRetrievalOrder() @@ -78,6 +85,12 @@ namespace osu.Game.Tests.Skins }); } + private Skin importSkinFromArchives(string filename) + { + var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); + return imported.PerformRead(skinInfo => skins.GetSkin(skinInfo)); + } + private class TestSkin : Skin { public const string SAMPLE_NAME = "test-sample"; diff --git a/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs b/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs index 185b83d1cc..271642edd1 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Background { public partial class TestSceneTriangleBorderShader : OsuTestScene { - private readonly TriangleBorder border; + private readonly TestTriangle triangle; public TestSceneTriangleBorderShader() { @@ -25,11 +25,11 @@ namespace osu.Game.Tests.Visual.Background RelativeSizeAxes = Axes.Both, Colour = Color4.DarkGreen }, - border = new TriangleBorder + triangle = new TestTriangle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(100) + Size = new Vector2(200) } }; } @@ -38,12 +38,13 @@ namespace osu.Game.Tests.Visual.Background { base.LoadComplete(); - AddSliderStep("Thickness", 0f, 1f, 0.02f, t => border.Thickness = t); + AddSliderStep("Thickness", 0f, 1f, 0.15f, t => triangle.Thickness = t); + AddSliderStep("Texel size", 0f, 0.1f, 0f, t => triangle.TexelSize = t); } - private partial class TriangleBorder : Sprite + private partial class TestTriangle : Sprite { - private float thickness = 0.02f; + private float thickness = 0.15f; public float Thickness { @@ -55,6 +56,18 @@ namespace osu.Game.Tests.Visual.Background } } + private float texelSize; + + public float TexelSize + { + get => texelSize; + set + { + texelSize = value; + Invalidate(Invalidation.DrawNode); + } + } + [BackgroundDependencyLoader] private void load(ShaderManager shaders, IRenderer renderer) { @@ -62,29 +75,32 @@ namespace osu.Game.Tests.Visual.Background Texture = renderer.WhitePixel; } - protected override DrawNode CreateDrawNode() => new TriangleBorderDrawNode(this); + protected override DrawNode CreateDrawNode() => new TriangleDrawNode(this); - private class TriangleBorderDrawNode : SpriteDrawNode + private class TriangleDrawNode : SpriteDrawNode { - public new TriangleBorder Source => (TriangleBorder)base.Source; + public new TestTriangle Source => (TestTriangle)base.Source; - public TriangleBorderDrawNode(TriangleBorder source) + public TriangleDrawNode(TestTriangle source) : base(source) { } private float thickness; + private float texelSize; public override void ApplyState() { base.ApplyState(); thickness = Source.thickness; + texelSize = Source.texelSize; } public override void Draw(IRenderer renderer) { TextureShader.GetUniform<float>("thickness").UpdateValue(ref thickness); + TextureShader.GetUniform<float>("texelSize").UpdateValue(ref texelSize); base.Draw(renderer); } diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs index d3cdf928e8..378dd99664 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesBackground.cs @@ -5,6 +5,7 @@ using osu.Game.Graphics.Backgrounds; using osu.Framework.Graphics; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; +using osuTK; namespace osu.Game.Tests.Visual.Background { @@ -25,7 +26,10 @@ namespace osu.Game.Tests.Visual.Background { RelativeSizeAxes = Axes.Both, ColourLight = Color4.White, - ColourDark = Color4.Gray + ColourDark = Color4.Gray, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.9f) } }; } @@ -35,6 +39,8 @@ namespace osu.Game.Tests.Visual.Background base.LoadComplete(); AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); + AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s)); + AddToggleStep("Masking", m => triangles.Masking = m); } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs index ae1f3de6bf..01a2464b8e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneTrianglesV2Background.cs @@ -8,12 +8,14 @@ using osuTK; using osuTK.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Framework.Graphics.Colour; +using osu.Game.Graphics.Sprites; namespace osu.Game.Tests.Visual.Background { public partial class TestSceneTrianglesV2Background : OsuTestScene { private readonly TrianglesV2 triangles; + private readonly TrianglesV2 maskedTriangles; private readonly Box box; public TestSceneTrianglesV2Background() @@ -31,12 +33,20 @@ namespace osu.Game.Tests.Visual.Background Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), + Spacing = new Vector2(0, 10), Children = new Drawable[] { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Masked" + }, new Container { Size = new Vector2(500, 100), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Masking = true, CornerRadius = 40, Children = new Drawable[] @@ -54,9 +64,43 @@ namespace osu.Game.Tests.Visual.Background } } }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Non-masked" + }, new Container { Size = new Vector2(500, 100), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red + }, + maskedTriangles = new TrianglesV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both + } + } + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Gradient comparison box" + }, + new Container + { + Size = new Vector2(500, 100), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Masking = true, CornerRadius = 40, Child = box = new Box @@ -75,14 +119,16 @@ namespace osu.Game.Tests.Visual.Background AddSliderStep("Spawn ratio", 0f, 10f, 1f, s => { - triangles.SpawnRatio = s; + triangles.SpawnRatio = maskedTriangles.SpawnRatio = s; triangles.Reset(1234); + maskedTriangles.Reset(1234); }); - AddSliderStep("Thickness", 0f, 1f, 0.02f, t => triangles.Thickness = t); + AddSliderStep("Thickness", 0f, 1f, 0.02f, t => triangles.Thickness = maskedTriangles.Thickness = t); - AddStep("White colour", () => box.Colour = triangles.Colour = Color4.White); - AddStep("Vertical gradient", () => box.Colour = triangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); - AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); + AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White); + AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red)); + AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red)); + AddToggleStep("Masking", m => maskedTriangles.Masking = m); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs index 7728adecae..8b598a6a24 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to common point", () => { - var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre; + var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1).ScreenSpaceDrawQuad.Centre; InputManager.MoveMouseTo(pos); }); AddStep("right click", () => InputManager.Click(MouseButton.Right)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index ffb8a67c68..b14025c9d8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to controlpoint", () => { - var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre; + var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1).ScreenSpaceDrawQuad.Centre; InputManager.MoveMouseTo(pos); }); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index a40c7814e1..5aa2dd2ebf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; @@ -20,6 +21,8 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; using osu.Game.Storyboards; @@ -40,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] + private RealmAccess realm { get; set; } = null!; + private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty; public override void SetUpSteps() @@ -222,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing return beatmap != null && beatmap.DifficultyName == secondDifficultyName && set != null - && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified)); + && set.PerformRead(s => + s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified)); }); } @@ -325,6 +332,56 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); } + [Test] + public void TestCopyDifficultyDoesNotChangeCollections() + { + string originalDifficultyName = Guid.NewGuid().ToString(); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + + string originalMd5 = string.Empty; + BeatmapCollection collection = null!; + + AddStep("setup a collection with original beatmap", () => + { + collection = new BeatmapCollection("test copy"); + collection.BeatmapMD5Hashes.Add(originalMd5 = EditorBeatmap.BeatmapInfo.MD5Hash); + + realm.Write(r => + { + r.Add(collection); + }); + }); + + AddAssert("collection contains original beatmap", () => + !string.IsNullOrEmpty(originalMd5) && collection.BeatmapMD5Hashes.Contains(originalMd5)); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick()); + + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != originalDifficultyName; + }); + + AddStep("save without changes", () => Editor.Save()); + + AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash) + && collection.BeatmapMD5Hashes.Contains(originalMd5)); + + AddStep("clean up collection", () => + { + realm.Write(r => + { + r.Remove(collection); + }); + }); + } + [Test] public void TestCreateMultipleNewDifficultiesSucceeds() { @@ -395,5 +452,52 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); }); } + + [Test] + public void TestCreateNewDifficultyForInconvertibleRuleset() + { + Guid setId = Guid.Empty; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("save beatmap", () => Editor.Save()); + AddStep("try to create new taiko difficulty", () => Editor.CreateNewDifficulty(new TaikoRuleset().RulesetInfo)); + + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty"; + }); + AddAssert("new difficulty persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); + }); + + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] + { + new Hit + { + StartTime = 0 + }, + new Hit + { + StartTime = 1000 + } + })); + AddStep("save beatmap", () => Editor.Save()); + AddStep("try to create new catch difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo)); + + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty (1)"; + }); + AddAssert("new difficulty persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 3 && s.Files.Count == 3); + }); + } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 70118e0b67..b396b382ff 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Editing double lastStarRating = 0; double lastLength = 0; - AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(500, new TimingControlPoint())); + AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 600 })); AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); diff --git a/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs b/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs index 545b3c2cf4..f54f50795e 100644 --- a/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs +++ b/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs @@ -20,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay { var implementation = skin is LegacySkin ? CreateLegacyImplementation() - : CreateDefaultImplementation(); + : skin is ArgonSkin + ? CreateArgonImplementation() + : CreateDefaultImplementation(); implementation.Anchor = Anchor.Centre; implementation.Origin = Anchor.Centre; @@ -29,6 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); protected abstract Drawable CreateDefaultImplementation(); + protected virtual Drawable CreateArgonImplementation() => CreateDefaultImplementation(); protected abstract Drawable CreateLegacyImplementation(); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgressGraph.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs similarity index 93% rename from osu.Game.Tests/Visual/Gameplay/TestSceneSongProgressGraph.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs index 5a61502978..66671a506f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgressGraph.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs @@ -14,7 +14,7 @@ using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public partial class TestSceneSongProgressGraph : OsuTestScene + public partial class TestSceneDefaultSongProgressGraph : OsuTestScene { private TestSongProgressGraph graph; @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay graph.Objects = objects; } - private partial class TestSongProgressGraph : SongProgressGraph + private partial class TestSongProgressGraph : DefaultSongProgressGraph { public int CreationCount { get; private set; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 29fadd151f..5e1412d79b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestInputDoesntWorkWhenHUDHidden() { - SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault(); + ArgonSongProgress? getSongProgress() => hudOverlay.ChildrenOfType<ArgonSongProgress>().SingleOrDefault(); bool seeked = false; @@ -204,8 +204,8 @@ namespace osu.Game.Tests.Visual.Gameplay Debug.Assert(progress != null); - progress.ShowHandle = true; - progress.OnSeek += _ => seeked = true; + progress.Interactive.Value = true; + progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true; }); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs new file mode 100644 index 0000000000..5a802e0d36 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs @@ -0,0 +1,182 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD.JudgementCounter; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneJudgementCounter : OsuTestScene + { + private ScoreProcessor scoreProcessor = null!; + private JudgementTally judgementTally = null!; + private TestJudgementCounterDisplay counterDisplay = null!; + + private DependencyProvidingContainer content = null!; + + protected override Container<Drawable> Content => content; + + private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>(); + + private int iteration; + + [SetUpSteps] + public void SetUpSteps() => AddStep("Create components", () => + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + scoreProcessor = new ScoreProcessor(ruleset); + base.Content.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) }, + Children = new Drawable[] + { + judgementTally = new JudgementTally(), + content = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) }, + } + }, + }; + }); + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + + private void applyOneJudgement(HitResult result) + { + lastJudgementResult.Value = new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000 + }, new OsuJudgement()) + { + Type = result, + }; + scoreProcessor.ApplyResult(lastJudgementResult.Value); + + iteration++; + } + + [Test] + public void TestAddJudgementsToCounters() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2); + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2); + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2); + } + + [Test] + public void TestAddWhilstHidden() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2); + AddAssert("Check value added whilst hidden", () => hiddenCount() == 2); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + } + + [Test] + public void TestChangeFlowDirection() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); + AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); + } + + [Test] + public void TestToggleJudgementNames() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false); + AddWaitStep("wait some", 2); + AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0); + AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = true); + AddWaitStep("wait some", 2); + AddAssert("Assert shown", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 1); + } + + [Test] + public void TestHideMaxValue() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false); + AddWaitStep("wait some", 2); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0); + AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true); + } + + [Test] + public void TestMaxValueStartsHidden() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay + { + ShowMaxJudgement = { Value = false } + }); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0); + } + + [Test] + public void TestMaxValueHiddenOnModeChange() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddStep("Set max judgement to hide itself", () => counterDisplay.ShowMaxJudgement.Value = false); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + AddWaitStep("wait some", 2); + AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0); + } + + [Test] + public void TestCycleDisplayModes() + { + AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay()); + + AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple); + AddWaitStep("wait some", 2); + AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 0); + AddStep("Show normal judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Normal); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + AddWaitStep("wait some", 2); + AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 1); + } + + private int hiddenCount() + { + var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit); + return num.Result.ResultCount.Value; + } + + private partial class TestJudgementCounterDisplay : JudgementCounterDisplay + { + public new FillFlowContainer<JudgementCounter> CounterFlow => base.CounterFlow; + + public TestJudgementCounterDisplay() + { + Margin = new MarginPadding { Top = 100 }; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 7880a849a2..b072ce191e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -5,9 +5,11 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Configuration; @@ -34,6 +36,12 @@ namespace osu.Game.Tests.Visual.Gameplay base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both }); } + [BackgroundDependencyLoader] + private void load() + { + LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0); + } + [SetUpSteps] public override void SetUpSteps() { @@ -43,6 +51,22 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(true); } + [Test] + public void TestTogglePauseViaBackAction() + { + pauseViaBackAction(); + pauseViaBackAction(); + confirmPausedWithNoOverlay(); + } + + [Test] + public void TestTogglePauseViaPauseGameplayAction() + { + pauseViaPauseGameplayAction(); + pauseViaPauseGameplayAction(); + confirmPausedWithNoOverlay(); + } + [Test] public void TestPauseWithLargeOffset() { @@ -66,7 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay Player.OnUpdate += _ => { double currentTime = Player.GameplayClockContainer.CurrentTime; - alwaysGoingForward &= currentTime >= lastTime - 500; + bool goingForward = currentTime >= lastTime - 500; + + alwaysGoingForward &= goingForward; + + if (!goingForward) + Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})"); + lastTime = currentTime; }; }); @@ -144,7 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("disable pause support", () => Player.Configuration.AllowPause = false); - pauseFromUserExitKey(); + pauseViaBackAction(); confirmExited(); } @@ -156,7 +186,7 @@ namespace osu.Game.Tests.Visual.Gameplay pauseAndConfirm(); resume(); - pauseFromUserExitKey(); + pauseViaBackAction(); confirmResumed(); confirmNotExited(); @@ -170,7 +200,7 @@ namespace osu.Game.Tests.Visual.Gameplay pauseAndConfirm(); resume(); - AddStep("pause via exit key", () => Player.ExitViaQuickExit()); + exitViaQuickExitAction(); confirmResumed(); AddAssert("exited", () => !Player.IsCurrentScreen()); @@ -214,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(false); - AddStep("exit via user pause", () => Player.ExitViaPause()); + pauseViaBackAction(); confirmExited(); } @@ -224,11 +254,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); // will finish the fail animation and show the fail/pause screen. - AddStep("attempt exit via pause key", () => Player.ExitViaPause()); + pauseViaBackAction(); AddAssert("fail overlay shown", () => Player.FailOverlayVisible); // will actually exit. - AddStep("exit via pause key", () => Player.ExitViaPause()); + pauseViaBackAction(); confirmExited(); } @@ -245,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestQuickExitFromFailedGameplay() { AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); - AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyExitOverlay>().First().Action?.Invoke()); + exitViaQuickExitAction(); confirmExited(); } @@ -261,7 +291,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromGameplay() { - AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyExitOverlay>().First().Action?.Invoke()); + exitViaQuickExitAction(); confirmExited(); } @@ -327,7 +357,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void pauseAndConfirm() { - pauseFromUserExitKey(); + pauseViaBackAction(); confirmPaused(); } @@ -374,7 +404,17 @@ namespace osu.Game.Tests.Visual.Gameplay } private void restart() => AddStep("restart", () => Player.Restart()); - private void pauseFromUserExitKey() => AddStep("user pause", () => Player.ExitViaPause()); + private void pauseViaBackAction() => AddStep("press escape", () => InputManager.Key(Key.Escape)); + private void pauseViaPauseGameplayAction() => AddStep("press middle mouse", () => InputManager.Click(MouseButton.Middle)); + + private void exitViaQuickExitAction() => AddStep("press ctrl-tilde", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(Key.Tilde); + InputManager.ReleaseKey(Key.Tilde); + InputManager.ReleaseKey(Key.ControlLeft); + }); + private void resume() => AddStep("resume", () => Player.Resume()); private void confirmPauseOverlayShown(bool isShown) => @@ -405,10 +445,6 @@ namespace osu.Game.Tests.Visual.Gameplay public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; - public void ExitViaPause() => PerformExit(true); - - public void ExitViaQuickExit() => PerformExit(false); - public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 2e579cc522..5855838d3c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -2,14 +2,14 @@ // 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.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; @@ -28,50 +28,62 @@ namespace osu.Game.Tests.Visual.Gameplay { Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); + FrameStabilityContainer frameStabilityContainer; + + Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time) + { + Child = frameStabilityContainer = new FrameStabilityContainer + { + MaxCatchUpFrames = 1 + } + }); Dependencies.CacheAs<IGameplayClock>(gameplayClockContainer); + Dependencies.CacheAs<IFrameStableClock>(frameStabilityContainer); } [SetUpSteps] public void SetupSteps() { AddStep("reset clock", () => gameplayClockContainer.Reset()); - AddStep("set hit objects", setHitObjects); + AddStep("set hit objects", () => this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = Beatmap.Value.Beatmap.HitObjects)); + AddStep("hook seeking", () => + { + applyToDefaultProgress(d => d.ChildrenOfType<DefaultSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t)); + applyToArgonProgress(d => d.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t)); + }); + AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time)); + AddStep("start", () => gameplayClockContainer.Start()); } [Test] - public void TestDisplay() + public void TestBasic() { - AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time)); - AddStep("start", gameplayClockContainer.Start); + AddToggleStep("toggle seeking", b => + { + applyToDefaultProgress(s => s.Interactive.Value = b); + applyToArgonProgress(s => s.Interactive.Value = b); + }); + + AddToggleStep("toggle graph", b => + { + applyToDefaultProgress(s => s.ShowGraph.Value = b); + applyToArgonProgress(s => s.ShowGraph.Value = b); + }); + AddStep("stop", gameplayClockContainer.Stop); } - [Test] - public void TestToggleSeeking() - { - void applyToDefaultProgress(Action<DefaultSongProgress> action) => - this.ChildrenOfType<DefaultSongProgress>().ForEach(action); + private void applyToArgonProgress(Action<ArgonSongProgress> action) => + this.ChildrenOfType<ArgonSongProgress>().ForEach(action); - AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true)); - AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false)); - AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false)); - AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true)); - AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true)); - } - - private void setHitObjects() - { - var objects = new List<HitObject>(); - for (double i = 0; i < 5000; i++) - objects.Add(new HitObject { StartTime = i }); - - this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = objects); - } + private void applyToDefaultProgress(Action<DefaultSongProgress> action) => + this.ChildrenOfType<DefaultSongProgress>().ForEach(action); protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress(); + protected override Drawable CreateArgonImplementation() => new ArgonSongProgress(); + protected override Drawable CreateLegacyImplementation() => new LegacySongProgress(); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 0e9863a9f5..0f1ba9ba75 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -16,6 +16,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; @@ -85,6 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>(); AddUntilStep("wait for join", () => MultiplayerClient.RoomJoined); + AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 7b1abd104f..869b8bb328 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.Play; @@ -33,6 +32,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("first item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID); } + [Test] + public void TestSingleItemExpiredAfterGameplay() + { + RunGameplay(); + + AddUntilStep("playlist has only one item", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 1); + AddUntilStep("playlist item is expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true); + AddUntilStep("last item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID); + } + [Test] public void TestItemAddedToTheEndOfQueue() { @@ -45,16 +54,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("first item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID); } - [Test] - public void TestSingleItemExpiredAfterGameplay() - { - RunGameplay(); - - AddUntilStep("playlist has only one item", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 1); - AddUntilStep("playlist item is expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true); - AddUntilStep("last item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID); - } - [Test] public void TestNextItemSelectedAfterGameplayFinish() { @@ -140,7 +139,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null); AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded); - AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value); if (ruleset != null) AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index 145c655c0a..78baa4a39b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -27,6 +27,28 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("first item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID); } + [Test] + public void TestNewItemCreatedAfterGameplayFinished() + { + RunGameplay(); + + AddUntilStep("playlist contains two items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2); + AddUntilStep("first playlist item expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true); + AddUntilStep("second playlist item not expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Expired == false); + AddUntilStep("second playlist item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[1].ID); + } + + [Test] + public void TestSettingsUpdatedWhenChangingQueueMode() + { + AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings + { + QueueMode = QueueMode.AllPlayers + }).WaitSafely()); + + AddUntilStep("api room updated", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers); + } + [Test] public void TestItemStillSelectedAfterChangeToSameBeatmap() { @@ -43,17 +65,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("playlist item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID); } - [Test] - public void TestNewItemCreatedAfterGameplayFinished() - { - RunGameplay(); - - AddUntilStep("playlist contains two items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2); - AddUntilStep("first playlist item expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true); - AddUntilStep("second playlist item not expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Expired == false); - AddUntilStep("second playlist item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[1].ID); - } - [Test] public void TestOnlyLastItemChangedAfterGameplayFinished() { @@ -68,17 +79,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("second playlist item changed", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Beatmap != firstBeatmap); } - [Test] - public void TestSettingsUpdatedWhenChangingQueueMode() - { - AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings - { - QueueMode = QueueMode.AllPlayers - }).WaitSafely()); - - AddUntilStep("api room updated", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers); - } - [Test] public void TestAddItemsAsHost() { @@ -104,7 +104,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); BeatmapInfo otherBeatmap = null; - AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); @@ -120,7 +119,6 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); - AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index c2036984c1..e09496b6e9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p => !p.ChildrenOfType<PlayerSettingsOverlay>().Any() && !p.ChildrenOfType<HoldForMenuButton>().Any() && - p.ChildrenOfType<SongProgressBar>().SingleOrDefault()?.ShowHandle == false)); + p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false)); AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 5033347b15..d747d23229 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -377,6 +377,17 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] + /* + * On a slight investigation, this is occurring due to the ready button + * not receiving the click input generated by the manual input manager. + * + * TearDown : System.TimeoutException : "wait for ready button to be enabled" timed out + * --TearDown + * at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0() + * at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) + * at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) + */ public void TestUserSetToIdleWhenBeatmapDeleted() { createRoom(() => new Room @@ -398,6 +409,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect() { PlaylistItem? item = null; @@ -438,6 +450,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestPlayStartsWithCorrectRulesetWhileAtSongSelect() { PlaylistItem? item = null; @@ -478,6 +491,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestPlayStartsWithCorrectModsWhileAtSongSelect() { PlaylistItem? item = null; @@ -651,6 +665,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestGameplayFlow() { createRoom(() => new Room @@ -678,6 +693,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestGameplayExitFlow() { Bindable<double>? holdDelay = null; @@ -715,6 +731,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestGameplayDoesntStartWithNonLoadedUser() { createRoom(() => new Room @@ -796,6 +813,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestSpectatingStateResetOnBackButtonDuringGameplay() { createRoom(() => new Room @@ -831,6 +849,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestSpectatingStateNotResetOnBackButtonOutsideOfGameplay() { createRoom(() => new Room @@ -869,6 +888,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestItemAddedByOtherUserDuringGameplay() { createRoom(() => new Room @@ -899,6 +919,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] + [FlakyTest] // See above public void TestItemAddedAndDeletedByOtherUserDuringGameplay() { createRoom(() => new Room diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 7bde2e747d..0d081e8138 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -563,6 +563,18 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("featured artist filter is off", () => !getBeatmapListingOverlay().ChildrenOfType<BeatmapSearchGeneralFilterRow>().First().Current.Contains(SearchGeneral.FeaturedArtists)); } + [Test] + public void TestBeatmapListingLinkSearchOnInitialOpen() + { + BeatmapListingOverlay getBeatmapListingOverlay() => Game.ChildrenOfType<BeatmapListingOverlay>().FirstOrDefault(); + + AddStep("open beatmap overlay with test query", () => Game.SearchBeatmapSet("test")); + + AddUntilStep("wait for beatmap overlay to load", () => getBeatmapListingOverlay()?.State.Value == Visibility.Visible); + + AddAssert("beatmap overlay sorted by relevance", () => getBeatmapListingOverlay().ChildrenOfType<BeatmapListingSortTabControl>().Single().Current.Value == SortCriteria.Relevance); + } + [Test] public void TestMainOverlaysClosesNotificationOverlay() { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index e0b61794e4..845cfdd6df 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -219,7 +219,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Click gameplay scene button", () => { - InputManager.MoveMouseTo(skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay")); + InputManager.MoveMouseTo(skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text.ToString() == "Gameplay")); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs index 0c165bc40e..25cef8440a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupBeatmapDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; @@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestBeatmapLink() { AddUntilStep("Beatmap overlay displayed", () => Game.ChildrenOfType<BeatmapSetOverlay>().FirstOrDefault()?.State.Value == Visibility.Visible); - AddUntilStep("Beatmap overlay showing content", () => Game.ChildrenOfType<BeatmapPicker>().FirstOrDefault()?.Beatmap.Value.OnlineID == requested_beatmap_id); + AddUntilStep("Beatmap overlay showing content", () => Game.ChildrenOfType<BeatmapPicker>().FirstOrDefault()?.Beatmap.Value?.OnlineID == requested_beatmap_id); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs index d925141510..dbf3b52572 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs @@ -11,6 +11,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -21,6 +23,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Comments; +using osu.Game.Overlays.Comments.Buttons; using osuTK.Input; namespace osu.Game.Tests.Visual.Online @@ -259,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any()); AddStep("Set report data", () => { - var field = this.ChildrenOfType<OsuTextBox>().Single(); + var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().Single(); field.Current.Value = report_text; var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single(); reason.Current.Value = CommentReportReason.Other; @@ -278,6 +281,93 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Request is correct", () => request != null && request.CommentID == 2 && request.Comment == report_text && request.Reason == CommentReportReason.Other); } + [Test] + public void TestReply() + { + addTestComments(); + DrawableComment? targetComment = null; + AddUntilStep("Comment exists", () => + { + var comments = this.ChildrenOfType<DrawableComment>(); + targetComment = comments.SingleOrDefault(x => x.Comment.Id == 2); + return targetComment != null; + }); + AddStep("Setup request handling", () => + { + requestLock.Reset(); + + dummyAPI.HandleRequest = r => + { + if (!(r is CommentPostRequest req)) + return false; + + if (req.ParentCommentId != 2) + throw new ArgumentException("Wrong parent ID in request!"); + + if (req.CommentableId != 123 || req.Commentable != CommentableType.Beatmapset) + throw new ArgumentException("Wrong commentable data in request!"); + + Task.Run(() => + { + requestLock.Wait(10000); + req.TriggerSuccess(new CommentBundle + { + Comments = new List<Comment> + { + new Comment + { + Id = 98, + Message = req.Message, + LegacyName = "FirstUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 98, + ParentId = req.ParentCommentId, + } + } + }); + }); + + return true; + }; + }); + AddStep("Click reply button", () => + { + var btn = targetComment.ChildrenOfType<LinkFlowContainer>().Skip(1).First(); + var texts = btn.ChildrenOfType<SpriteText>(); + InputManager.MoveMouseTo(texts.Skip(1).First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("There is 0 replies", () => + { + var replLabel = targetComment.ChildrenOfType<ShowRepliesButton>().First().ChildrenOfType<SpriteText>().First(); + return replLabel.Text.ToString().Contains('0') && targetComment!.Comment.RepliesCount == 0; + }); + AddStep("Focus field", () => + { + InputManager.MoveMouseTo(targetComment.ChildrenOfType<TextBox>().First()); + InputManager.Click(MouseButton.Left); + }); + AddStep("Enter text", () => + { + targetComment.ChildrenOfType<TextBox>().First().Current.Value = "random reply"; + }); + AddStep("Submit", () => + { + InputManager.Key(Key.Enter); + }); + AddStep("Complete request", () => requestLock.Set()); + AddUntilStep("There is 1 reply", () => + { + var replLabel = targetComment.ChildrenOfType<ShowRepliesButton>().First().ChildrenOfType<SpriteText>().First(); + return replLabel.Text.ToString().Contains('1') && targetComment!.Comment.RepliesCount == 1; + }); + AddUntilStep("Submitted comment shown", () => + { + var r = targetComment.ChildrenOfType<DrawableComment>().Skip(1).FirstOrDefault(); + return r != null && r.Comment.Message == "random reply"; + }); + } + private void addTestComments() { AddStep("set up response", () => diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 291ccd634d..3d8781d902 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -11,7 +9,10 @@ using osu.Game.Overlays; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -27,15 +28,20 @@ namespace osu.Game.Tests.Visual.Online private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; - private CommentsContainer commentsContainer; + private CommentsContainer commentsContainer = null!; + + private TextBox editorTextBox = null!; [SetUp] public void SetUp() => Schedule(() => + { Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, Child = commentsContainer = new CommentsContainer() - }); + }; + editorTextBox = commentsContainer.ChildrenOfType<TextBox>().First(); + }); [Test] public void TestIdleState() @@ -126,6 +132,44 @@ namespace osu.Game.Tests.Visual.Online commentsContainer.ChildrenOfType<DrawableComment>().Count(d => d.Comment.Pinned == withPinned) == 1); } + [Test] + public void TestPost() + { + setUpCommentsResponse(new CommentBundle { Comments = new List<Comment>() }); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddAssert("no comments placeholder shown", () => commentsContainer.ChildrenOfType<CommentsContainer.NoCommentsPlaceholder>().Any()); + + setUpPostResponse(); + AddStep("enter text", () => editorTextBox.Current.Value = "comm"); + AddStep("submit", () => commentsContainer.ChildrenOfType<RoundedButton>().First().TriggerClick()); + + AddUntilStep("comment sent", () => + { + string writtenText = editorTextBox.Current.Value; + var comment = commentsContainer.ChildrenOfType<DrawableComment>().LastOrDefault(); + return comment != null && comment.ChildrenOfType<SpriteText>().Any(y => y.Text == writtenText); + }); + AddAssert("no comments placeholder removed", () => !commentsContainer.ChildrenOfType<CommentsContainer.NoCommentsPlaceholder>().Any()); + } + + [Test] + public void TestPostWithExistingComments() + { + setUpCommentsResponse(getExampleComments()); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + + setUpPostResponse(); + AddStep("enter text", () => editorTextBox.Current.Value = "comm"); + AddStep("submit", () => commentsContainer.ChildrenOfType<CommentEditor>().Single().ChildrenOfType<RoundedButton>().First().TriggerClick()); + + AddUntilStep("comment sent", () => + { + string writtenText = editorTextBox.Current.Value; + var comment = commentsContainer.ChildrenOfType<DrawableComment>().LastOrDefault(); + return comment != null && comment.ChildrenOfType<SpriteText>().Any(y => y.Text == writtenText); + }); + } + private void setUpCommentsResponse(CommentBundle commentBundle) => AddStep("set up response", () => { @@ -139,7 +183,33 @@ namespace osu.Game.Tests.Visual.Online }; }); - private CommentBundle getExampleComments(bool withPinned = false) + private void setUpPostResponse() + => AddStep("set up response", () => + { + dummyAPI.HandleRequest = request => + { + if (!(request is CommentPostRequest req)) + return false; + + req.TriggerSuccess(new CommentBundle + { + Comments = new List<Comment> + { + new Comment + { + Id = 98, + Message = req.Message, + LegacyName = "FirstUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 98, + } + } + }); + return true; + }; + }); + + private static CommentBundle getExampleComments(bool withPinned = false) { var bundle = new CommentBundle { diff --git a/osu.Game.Tests/Visual/Online/TestSceneLevelBadge.cs b/osu.Game.Tests/Visual/Online/TestSceneLevelBadge.cs new file mode 100644 index 0000000000..1fd54ca50f --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneLevelBadge.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public partial class TestSceneLevelBadge : OsuTestScene + { + public TestSceneLevelBadge() + { + var levels = new List<UserStatistics.LevelInfo>(); + + for (int i = 0; i < 11; i++) + { + levels.Add(new UserStatistics.LevelInfo + { + Current = i * 10 + }); + } + + levels.Add(new UserStatistics.LevelInfo { Current = 101 }); + levels.Add(new UserStatistics.LevelInfo { Current = 105 }); + levels.Add(new UserStatistics.LevelInfo { Current = 110 }); + levels.Add(new UserStatistics.LevelInfo { Current = 115 }); + levels.Add(new UserStatistics.LevelInfo { Current = 120 }); + + Children = new Drawable[] + { + new FillFlowContainer<LevelBadge> + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Full, + Spacing = new Vector2(5), + ChildrenEnumerable = levels.Select(level => new LevelBadge + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(60), + LevelInfo = { Value = level } + }) + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 513c473830..640e895b6c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -19,6 +20,9 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + private ProfileHeader header = null!; [SetUpSteps] @@ -33,6 +37,22 @@ namespace osu.Game.Tests.Visual.Online AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo)); } + [Test] + public void TestProfileCoverExpanded() + { + AddStep("Set cover to expanded", () => configManager.SetValue(OsuSetting.ProfileCoverExpanded, true)); + AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo)); + AddUntilStep("Cover is expanded", () => header.ChildrenOfType<UserCoverBackground>().Single().Height, () => Is.GreaterThan(0)); + } + + [Test] + public void TestProfileCoverCollapsed() + { + AddStep("Set cover to collapsed", () => configManager.SetValue(OsuSetting.ProfileCoverExpanded, false)); + AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo)); + AddUntilStep("Cover is collapsed", () => header.ChildrenOfType<UserCoverBackground>().Single().Height, () => Is.EqualTo(0)); + } + [Test] public void TestOnlineState() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index f7e88e4437..9aaa616c04 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -82,13 +82,14 @@ namespace osu.Game.Tests.Visual.Online { Username = @"Somebody", Id = 1, - CountryCode = CountryCode.Unknown, + CountryCode = CountryCode.JP, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", JoinDate = DateTimeOffset.Now.AddDays(-1), LastVisit = DateTimeOffset.Now, Groups = new[] { new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" }, + new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } }, new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } } }, ProfileOrder = new[] @@ -142,7 +143,8 @@ namespace osu.Game.Tests.Visual.Online { Available = 10, Total = 50 - } + }, + SupportLevel = 2, }; } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index b353123649..0aa0295f7d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -123,7 +123,7 @@ needs_cleanup: true AddStep("Add absolute image", () => { markdownContainer.CurrentPath = "https://dev.ppy.sh"; - markdownContainer.Text = ""; + markdownContainer.Text = ""; }); } @@ -133,7 +133,7 @@ needs_cleanup: true AddStep("Add relative image", () => { markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; - markdownContainer.Text = ""; + markdownContainer.Text = ""; }); } @@ -145,7 +145,7 @@ needs_cleanup: true markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.Text = @"Line before image - + Line after image"; }); @@ -170,12 +170,12 @@ Line after image"; markdownContainer.Text = @" | Image | Name | Effect | | :-: | :-: | :-- | -|  | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. | -|  | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. | -|  | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. | -|   | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. | -|  | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. | -|  | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. | +|  | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. | +|  | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. | +|  | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. | +|   | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. | +|  | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. | +|  | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. | "; }); } @@ -186,7 +186,7 @@ Line after image"; AddStep("Add image", () => { markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/"; - markdownContainer.Text = ""; + markdownContainer.Text = ""; }); AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted); @@ -270,6 +270,30 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed }); } + [Test] + public void TestCodeSyntax() + { + AddStep("set content", () => + { + markdownContainer.Text = @" +This is a paragraph containing `inline code` synatax. +Oh wow I do love the `WikiMarkdownContainer`, it is very cool! + +This is a line before the fenced code block: +```csharp +public class WikiMarkdownContainer : MarkdownContainer +{ + public WikiMarkdownContainer() + { + this.foo = bar; + } +} +``` +This is a line after the fenced code block! +"; + }); + } + private partial class TestMarkdownContainer : WikiMarkdownContainer { public LinkInline Link; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index bdae91de59..6c732f4295 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType<MatchLeaderboardScore>().Count(s => s.ScoreText.Text != "0") == 2); - AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick()); + ClickButtonWhenEnabled<PlaylistsReadyButton>(); AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index ea9508ecff..65ce0429fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -580,10 +580,9 @@ namespace osu.Game.Tests.Visual.SongSelect /// Ensures stability is maintained on different sort modes for items with equal properties. /// </summary> [Test] - public void TestSortingStability() + public void TestSortingStabilityDateAdded() { var sets = new List<BeatmapSetInfo>(); - int idOffset = 0; AddStep("Populuate beatmap sets", () => { @@ -593,38 +592,34 @@ namespace osu.Game.Tests.Visual.SongSelect { var set = TestResources.CreateTestBeatmapSetInfo(); + set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i); + // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); - beatmap.Metadata.Artist = $"artist {i / 2}"; - beatmap.Metadata.Title = $"title {9 - i}"; + beatmap.Metadata.Artist = "a"; + beatmap.Metadata.Title = "b"; sets.Add(set); } - - idOffset = sets.First().OnlineID; }); loadBeatmaps(sets); - AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); - AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b)); - AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); - AddAssert("Items are in reverse order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + sets.Count - index - 1).All(b => b)); + AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); - AddAssert("Items reset to original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b)); + AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); } /// <summary> /// Ensures stability is maintained on different sort modes while a new item is added to the carousel. /// </summary> [Test] - public void TestSortingStabilityWithNewItems() + public void TestSortingStabilityWithRemovedAndReaddedItem() { List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>(); - int idOffset = 0; AddStep("Populuate beatmap sets", () => { @@ -640,16 +635,68 @@ namespace osu.Game.Tests.Visual.SongSelect beatmap.Metadata.Artist = "same artist"; beatmap.Metadata.Title = "same title"; + // testing the case where DateAdded happens to equal (quite rare). + set.DateAdded = DateTimeOffset.UnixEpoch; + sets.Add(set); } - - idOffset = sets.First().OnlineID; }); + Guid[] originalOrder = null!; + loadBeatmaps(sets); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); - assertOriginalOrderMaintained(); + + AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); + AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray()); + + AddStep("Remove item", () => carousel.RemoveBeatmapSet(sets[1])); + AddStep("Re-add item", () => carousel.UpdateBeatmapSet(sets[1])); + + AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); + + AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); + AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); + } + + /// <summary> + /// Ensures stability is maintained on different sort modes while a new item is added to the carousel. + /// </summary> + [Test] + public void TestSortingStabilityWithNewItems() + { + List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>(); + + AddStep("Populuate beatmap sets", () => + { + sets.Clear(); + + for (int i = 0; i < 3; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(3); + + // only need to set the first as they are a shared reference. + var beatmap = set.Beatmaps.First(); + + beatmap.Metadata.Artist = "same artist"; + beatmap.Metadata.Title = "same title"; + + // testing the case where DateAdded happens to equal (quite rare). + set.DateAdded = DateTimeOffset.UnixEpoch; + + sets.Add(set); + } + }); + + Guid[] originalOrder = null!; + + loadBeatmaps(sets); + + AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); + + AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending); + AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray()); AddStep("Add new item", () => { @@ -661,19 +708,18 @@ namespace osu.Game.Tests.Visual.SongSelect beatmap.Metadata.Artist = "same artist"; beatmap.Metadata.Title = "same title"; + set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(1); + carousel.UpdateBeatmapSet(set); + + // add set to expected ordering + originalOrder = originalOrder.Prepend(set.ID).ToArray(); }); - assertOriginalOrderMaintained(); + AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); - assertOriginalOrderMaintained(); - - void assertOriginalOrderMaintained() - { - AddAssert("Items remain in original order", - () => carousel.BeatmapSets.Select(s => s.OnlineID), () => Is.EqualTo(carousel.BeatmapSets.Select((set, index) => idOffset + index))); - } + AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder)); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index cfee02dfb8..feab86d3ee 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1064,6 +1064,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("options enabled", () => songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value); AddStep("delete all beatmaps", () => manager.Delete()); + AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault); AddAssert("options disabled", () => !songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs index 80941569af..1144b9053d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; @@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("beatmap density with granularity of 200", () => beatmapDensity()); AddStep("beatmap density with granularity of 300", () => beatmapDensity(300)); AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray()); - AddStep("change colour", () => + AddStep("change tier colours", () => { graph.TierColours = new[] { @@ -62,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface Colour4.Blue }; }); - AddStep("reset colour", () => + AddStep("reset tier colours", () => { graph.TierColours = new[] { @@ -74,6 +75,12 @@ namespace osu.Game.Tests.Visual.UserInterface Colour4.Green }; }); + + AddStep("set graph colour to blue", () => graph.Colour = Colour4.Blue); + AddStep("set graph colour to transparent", () => graph.Colour = Colour4.Transparent); + AddStep("set graph colour to vertical gradient", () => graph.Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Black)); + AddStep("set graph colour to horizontal gradient", () => graph.Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.Black)); + AddStep("reset graph colour", () => graph.Colour = Colour4.White); } private void sinFunction(int size = 100) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index 1a9122c117..f29272fbb8 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Tests { Colour = OsuColour.Gray(0.5f), Depth = 10 - }, AddInternal); + }, Add); // Have to construct this here, rather than in the constructor, because // we depend on some dependencies to be loaded within OsuGameBase.load(). diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs index 1bc576bc63..6db8605808 100644 --- a/osu.Game.Tournament/SaveChangesOverlay.cs +++ b/osu.Game.Tournament/SaveChangesOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tournament private async Task checkForChanges() { - string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder()).ConfigureAwait(false); + string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder()).ConfigureAwait(true); // If a save hasn't been triggered by the user yet, populate the initial value lastSerialisedLadder ??= serialisedLadder; diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index b86513eb49..ceddd4d1a1 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tournament.Screens.Setup new LabelledDropdown<RulesetInfo> { Label = "Ruleset", - Description = "Decides what stats are displayed and which ranks are retrieved for players.", + Description = "Decides what stats are displayed and which ranks are retrieved for players. This requires a restart to reload data for an existing bracket.", Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 08f21cb556..7e19cb3aa5 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -16,6 +16,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics; +using osu.Game.Online; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.IO; @@ -44,12 +45,20 @@ namespace osu.Game.Tournament return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); } + public override EndpointConfiguration CreateEndpoints() + { + if (UseDevelopmentServer) + return base.CreateEndpoints(); + + return new ProductionEndpointConfiguration(); + } + private TournamentSpriteText initialisationText; [BackgroundDependencyLoader] private void load(Storage baseStorage) { - AddInternal(initialisationText = new TournamentSpriteText + Add(initialisationText = new TournamentSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -156,9 +165,21 @@ namespace osu.Game.Tournament addedInfo |= addSeedingBeatmaps(); if (addedInfo) - SaveChanges(); + saveChanges(); ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + + ladder.Ruleset.BindValueChanged(r => + { + // Refetch player rank data on next startup as the ruleset has changed. + foreach (var team in ladder.Teams) + { + foreach (var player in team.Players) + player.Rank = null; + } + + SaveChanges(); + }); } catch (Exception e) { @@ -306,6 +327,11 @@ namespace osu.Game.Tournament return; } + saveChanges(); + } + + private void saveChanges() + { foreach (var r in ladder.Rounds) r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index eafd1e96e8..ad56bbbc3a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -136,14 +136,12 @@ namespace osu.Game.Beatmaps /// <param name="rulesetInfo">The ruleset with which the new difficulty should be created.</param> public virtual WorkingBeatmap CreateNewDifficulty(BeatmapSetInfo targetBeatmapSet, WorkingBeatmap referenceWorkingBeatmap, RulesetInfo rulesetInfo) { - var playableBeatmap = referenceWorkingBeatmap.GetPlayableBeatmap(rulesetInfo); - - var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), playableBeatmap.Metadata.DeepClone()) + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceWorkingBeatmap.Metadata.DeepClone()) { DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty") }; var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; - foreach (var timingPoint in playableBeatmap.ControlPointInfo.TimingPoints) + foreach (var timingPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); @@ -188,7 +186,7 @@ namespace osu.Game.Beatmaps targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo); newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet; - Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin); + save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false); workingBeatmapCache.Invalidate(targetBeatmapSet); return GetWorkingBeatmap(newBeatmap.BeatmapInfo); @@ -282,77 +280,16 @@ namespace osu.Game.Beatmaps public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; /// <summary> - /// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>. + /// Saves an existing <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>. /// </summary> + /// <remarks> + /// This method will also update any user beatmap collection hash references to the new post-saved hash. + /// </remarks> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param> /// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param> /// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param> - public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) - { - var setInfo = beatmapInfo.BeatmapSet; - Debug.Assert(setInfo != null); - - // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. - // This should hopefully be temporary, assuming said clone is eventually removed. - - // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved) - // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation). - // CopyTo() will undo such adjustments, while CopyFrom() will not. - beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty); - - // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. - beatmapContent.BeatmapInfo = beatmapInfo; - - using (var stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. - var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null; - string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); - - // ensure that two difficulties from the set don't point at the same beatmap file. - if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) - throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); - - if (existingFileInfo != null) - DeleteFile(setInfo, existingFileInfo); - - string oldMd5Hash = beatmapInfo.MD5Hash; - - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - beatmapInfo.Hash = stream.ComputeSHA2Hash(); - - beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; - beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; - - AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); - - updateHashAndMarkDirty(setInfo); - - Realm.Write(r => - { - var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID); - - setInfo.CopyChangesToRealm(liveBeatmapSet); - - beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); - - ProcessBeatmap?.Invoke((liveBeatmapSet, false)); - }); - } - - Debug.Assert(beatmapInfo.BeatmapSet != null); - - static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo) - { - var metadata = beatmapInfo.Metadata; - return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename(); - } - } + public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) => + save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true); public void DeleteAllVideos() { @@ -462,6 +399,74 @@ namespace osu.Game.Beatmaps setInfo.Status = BeatmapOnlineStatus.LocallyModified; } + private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections) + { + var setInfo = beatmapInfo.BeatmapSet; + Debug.Assert(setInfo != null); + + // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. + // This should hopefully be temporary, assuming said clone is eventually removed. + + // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved) + // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation). + // CopyTo() will undo such adjustments, while CopyFrom() will not. + beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty); + + // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. + beatmapContent.BeatmapInfo = beatmapInfo; + + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. + var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null; + string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); + + // ensure that two difficulties from the set don't point at the same beatmap file. + if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase))) + throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'."); + + if (existingFileInfo != null) + DeleteFile(setInfo, existingFileInfo); + + string oldMd5Hash = beatmapInfo.MD5Hash; + + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + beatmapInfo.Hash = stream.ComputeSHA2Hash(); + + beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; + beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + + AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); + + updateHashAndMarkDirty(setInfo); + + Realm.Write(r => + { + var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID); + + setInfo.CopyChangesToRealm(liveBeatmapSet); + + if (transferCollections) + beatmapInfo.TransferCollectionReferences(r, oldMd5Hash); + + ProcessBeatmap?.Invoke((liveBeatmapSet, false)); + }); + } + + Debug.Assert(beatmapInfo.BeatmapSet != null); + + static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo) + { + var metadata = beatmapInfo.Metadata; + return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename(); + } + } + #region Implementation of ICanAcceptFiles public Task Import(params string[] paths) => beatmapImporter.Import(paths); diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 29b7191ecf..1a15db98e4 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -183,9 +183,15 @@ namespace osu.Game.Beatmaps.ControlPoints private static double getClosestSnappedTime(TimingControlPoint timingPoint, double time, int beatDivisor) { double beatLength = timingPoint.BeatLength / beatDivisor; - int beatLengths = (int)Math.Round((time - timingPoint.Time) / beatLength, MidpointRounding.AwayFromZero); + double beats = (Math.Max(time, 0) - timingPoint.Time) / beatLength; - return timingPoint.Time + beatLengths * beatLength; + int roundedBeats = (int)Math.Round(beats, MidpointRounding.AwayFromZero); + double snappedTime = timingPoint.Time + roundedBeats * beatLength; + + if (snappedTime >= 0) + return snappedTime; + + return snappedTime + beatLength; } /// <summary> diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index e6f96330e7..76a31a6f78 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -268,7 +268,10 @@ namespace osu.Game.Beatmaps Stream storyboardFileStream = null; - if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename is string storyboardFilename) + string mainStoryboardFilename = getMainStoryboardFilename(BeatmapSetInfo.Metadata); + + if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.Equals(mainStoryboardFilename, StringComparison.OrdinalIgnoreCase))?.Filename is string + storyboardFilename) { string storyboardFileStorePath = BeatmapSetInfo?.GetPathForFile(storyboardFilename); storyboardFileStream = GetStream(storyboardFileStorePath); @@ -312,6 +315,33 @@ namespace osu.Game.Beatmaps } public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath); + + private string getMainStoryboardFilename(IBeatmapMetadataInfo metadata) + { + // Matches stable implementation, because it's probably simpler than trying to do anything else. + // This may need to be reconsidered after we begin storing storyboards in the new editor. + return windowsFilenameStrip( + (metadata.Artist.Length > 0 ? metadata.Artist + @" - " + metadata.Title : Path.GetFileNameWithoutExtension(metadata.AudioFile)) + + (metadata.Author.Username.Length > 0 ? @" (" + metadata.Author.Username + @")" : string.Empty) + + @".osb"); + + string windowsFilenameStrip(string entry) + { + // Inlined from Path.GetInvalidFilenameChars() to ensure the windows characters are used (to match stable). + char[] invalidCharacters = + { + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', + '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', + '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' + }; + + foreach (char c in invalidCharacters) + entry = entry.Replace(c.ToString(), string.Empty); + + return entry; + } + } } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 6cbb677a64..565a919fb8 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -58,8 +58,12 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal); + SetDefault(OsuSetting.ProfileCoverExpanded, true); + SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); + SetDefault(OsuSetting.SongSelectBackgroundBlur, true); + // Online settings SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); @@ -339,6 +343,7 @@ namespace osu.Game.Configuration ChatDisplayHeight, BeatmapListingCardSize, ToolbarClockDisplayMode, + SongSelectBackgroundBlur, Version, ShowFirstRunSetup, ShowConvertedBeatmaps, @@ -375,5 +380,6 @@ namespace osu.Game.Configuration LastProcessedMetadataId, SafeAreaConsiderations, ComboColourNormalisationAmount, + ProfileCoverExpanded, } } diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 94397f7ffb..6750d74a08 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -31,12 +31,6 @@ namespace osu.Game.Graphics.Backgrounds /// </summary> private const float equilateral_triangle_ratio = 0.866f; - /// <summary> - /// How many screen-space pixels are smoothed over. - /// Same behavior as Sprite's EdgeSmoothness. - /// </summary> - private const float edge_smoothness = 1; - private Color4 colourLight = Color4.White; public Color4 ColourLight @@ -83,6 +77,12 @@ namespace osu.Game.Graphics.Backgrounds set => triangleScale.Value = value; } + /// <summary> + /// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s + /// shape is drawn to the screen. + /// </summary> + public bool Masking { get; set; } + /// <summary> /// Whether we should drop-off alpha values of triangles more quickly to improve /// the visual appearance of fading. This defaults to on as it is generally more @@ -115,7 +115,7 @@ namespace osu.Game.Graphics.Backgrounds private void load(IRenderer renderer, ShaderManager shaders) { texture = renderer.WhitePixel; - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"); } protected override void LoadComplete() @@ -252,14 +252,18 @@ namespace osu.Game.Graphics.Backgrounds private class TrianglesDrawNode : DrawNode { + private float fill = 1f; + protected new Triangles Source => (Triangles)base.Source; private IShader shader; private Texture texture; + private bool masking; private readonly List<TriangleParticle> parts = new List<TriangleParticle>(); - private Vector2 size; + private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size; + private Vector2 size; private IVertexBatch<TexturedVertex2D> vertexBatch; public TrianglesDrawNode(Triangles source) @@ -274,6 +278,7 @@ namespace osu.Game.Graphics.Backgrounds shader = Source.shader; texture = Source.texture; size = Source.DrawSize; + masking = Source.Masking; parts.Clear(); parts.AddRange(Source.parts); @@ -290,34 +295,52 @@ namespace osu.Game.Graphics.Backgrounds } shader.Bind(); - - Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy; + shader.GetUniform<float>("thickness").UpdateValue(ref fill); foreach (TriangleParticle particle in parts) { - var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * equilateral_triangle_ratio); + Vector2 relativeSize = Vector2.Divide(triangleSize * particle.Scale, size); - var triangle = new Triangle( - Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix), - Vector2Extensions.Transform(particle.Position * size + offset, DrawInfo.Matrix), - Vector2Extensions.Transform(particle.Position * size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix) + Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); + + Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); + + var drawQuad = new Quad( + Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), + Vector2Extensions.Transform(triangleQuad.TopRight * size, DrawInfo.Matrix), + Vector2Extensions.Transform(triangleQuad.BottomLeft * size, DrawInfo.Matrix), + Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix) ); ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(particle.Colour); - renderer.DrawTriangle( - texture, - triangle, - colourInfo, - null, - vertexBatch.AddAction, - Vector2.Divide(localInflationAmount, new Vector2(2 * offset.X, offset.Y))); + RectangleF textureCoords = new RectangleF( + triangleQuad.TopLeft.X - topLeft.X, + triangleQuad.TopLeft.Y - topLeft.Y, + triangleQuad.Width, + triangleQuad.Height + ) / relativeSize; + + renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); } shader.Unbind(); } + private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + { + float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); + float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + + return new Quad( + leftClamped, + topClamped, + Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, + Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped + ); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index d543f082b4..3bc8bb6df1 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -34,6 +34,12 @@ namespace osu.Game.Graphics.Backgrounds /// </summary> protected virtual bool CreateNewTriangles => true; + /// <summary> + /// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s + /// shape is drawn to the screen. + /// </summary> + public bool Masking { get; set; } + private readonly BindableFloat spawnRatio = new BindableFloat(1f); /// <summary> @@ -189,6 +195,7 @@ namespace osu.Game.Graphics.Backgrounds private Vector2 size; private float thickness; private float texelSize; + private bool masking; private IVertexBatch<TexturedVertex2D>? vertexBatch; @@ -205,6 +212,7 @@ namespace osu.Game.Graphics.Backgrounds texture = Source.texture; size = Source.DrawSize; thickness = Source.Thickness; + masking = Source.Masking; Quad triangleQuad = new Quad( Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix), @@ -236,26 +244,31 @@ namespace osu.Game.Graphics.Backgrounds shader.GetUniform<float>("thickness").UpdateValue(ref thickness); shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize); - float relativeHeight = triangleSize.Y / size.Y; - float relativeWidth = triangleSize.X / size.X; + Vector2 relativeSize = Vector2.Divide(triangleSize, size); foreach (TriangleParticle particle in parts) { - Vector2 topLeft = particle.Position - new Vector2(relativeWidth * 0.5f, 0f); - Vector2 topRight = topLeft + new Vector2(relativeWidth, 0f); - Vector2 bottomLeft = topLeft + new Vector2(0f, relativeHeight); - Vector2 bottomRight = bottomLeft + new Vector2(relativeWidth, 0f); + Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f); + + Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y); var drawQuad = new Quad( - Vector2Extensions.Transform(topLeft * size, DrawInfo.Matrix), - Vector2Extensions.Transform(topRight * size, DrawInfo.Matrix), - Vector2Extensions.Transform(bottomLeft * size, DrawInfo.Matrix), - Vector2Extensions.Transform(bottomRight * size, DrawInfo.Matrix) + Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix), + Vector2Extensions.Transform(triangleQuad.TopRight * size, DrawInfo.Matrix), + Vector2Extensions.Transform(triangleQuad.BottomLeft * size, DrawInfo.Matrix), + Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix) ); - ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, new Quad(topLeft, topRight, bottomLeft, bottomRight)); + ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, triangleQuad); - renderer.DrawQuad(texture, drawQuad, colourInfo, vertexAction: vertexBatch.AddAction); + RectangleF textureCoords = new RectangleF( + triangleQuad.TopLeft.X - topLeft.X, + triangleQuad.TopLeft.Y - topLeft.Y, + triangleQuad.Width, + triangleQuad.Height + ) / relativeSize; + + renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); } shader.Unbind(); @@ -272,6 +285,19 @@ namespace osu.Game.Graphics.Backgrounds }; } + private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) + { + float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); + float topClamped = Math.Clamp(topLeft.Y, 0f, 1f); + + return new Quad( + leftClamped, + topClamped, + Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped, + Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped + ); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs index 5f5b9acf56..dbc358882c 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownTextFlowContainer.cs @@ -42,8 +42,12 @@ namespace osu.Game.Graphics.Containers.Markdown protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink) => AddDrawable(new OsuMarkdownFootnoteBacklink(footnoteBacklink)); - protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic) - => CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic)); + protected override void ApplyEmphasisedCreationParameters(SpriteText spriteText, bool bold, bool italic) + { + base.ApplyEmphasisedCreationParameters(spriteText, bold, italic); + + spriteText.Font = spriteText.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic); + } protected override void AddCustomComponent(CustomContainerInline inline) { diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index c5659aaf57..e06f6b3fd0 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Colour; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -187,6 +188,41 @@ namespace osu.Game.Graphics } } + /// <summary> + /// Retrieves colour for a <see cref="RankingTier"/>. + /// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours + /// </summary> + public ColourInfo ForRankingTier(RankingTier tier) + { + switch (tier) + { + default: + case RankingTier.Iron: + return Color4Extensions.FromHex(@"BAB3AB"); + + case RankingTier.Bronze: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"B88F7A"), Color4Extensions.FromHex(@"855C47")); + + case RankingTier.Silver: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"E0E0EB"), Color4Extensions.FromHex(@"A3A3C2")); + + case RankingTier.Gold: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"F0E4A8"), Color4Extensions.FromHex(@"E0C952")); + + case RankingTier.Platinum: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"A8F0EF"), Color4Extensions.FromHex(@"52E0DF")); + + case RankingTier.Rhodium: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"D9F8D3"), Color4Extensions.FromHex(@"A0CF96")); + + case RankingTier.Radiant: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"97DCFF"), Color4Extensions.FromHex(@"ED82FF")); + + case RankingTier.Lustrous: + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"FFE600"), Color4Extensions.FromHex(@"ED82FF")); + } + } + /// <summary> /// Returns a foreground text colour that is supposed to contrast well with /// the supplied <paramref name="backgroundColour"/>. diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 038ea0f5d7..7aa98ece95 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -3,6 +3,7 @@ #nullable disable +using System.ComponentModel; using osu.Framework.Graphics.Sprites; namespace osu.Game.Graphics @@ -115,6 +116,8 @@ namespace osu.Game.Graphics { Venera, Torus, + + [Description("Torus (alternate)")] TorusAlternate, Inter, } diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs index 9d65d6b8b9..20461de08f 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; @@ -13,12 +11,12 @@ namespace osu.Game.Graphics.UserInterface { public readonly MenuItemType Type; - public OsuMenuItem(string text, MenuItemType type = MenuItemType.Standard) + public OsuMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard) : this(text, type, null) { } - public OsuMenuItem(LocalisableString text, MenuItemType type, Action action) + public OsuMenuItem(LocalisableString text, MenuItemType type, Action? action) : base(text, action) { Type = type; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index a65204e6b3..99803e2956 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -250,13 +250,16 @@ namespace osu.Game.Graphics.UserInterface protected override void OnFocus(FocusEvent e) { - BorderThickness = 3; + if (Masking) + BorderThickness = 3; + base.OnFocus(e); } protected override void OnFocusLost(FocusLostEvent e) { - BorderThickness = 0; + if (Masking) + BorderThickness = 0; base.OnFocusLost(e); } diff --git a/osu.Game/Graphics/UserInterface/SegmentedGraph.cs b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs index 8ccba710b8..e8817d5275 100644 --- a/osu.Game/Graphics/UserInterface/SegmentedGraph.cs +++ b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; @@ -48,17 +49,14 @@ namespace osu.Game.Graphics.UserInterface } } - private Colour4[] tierColours; + private IReadOnlyList<Colour4> tierColours; - public Colour4[] TierColours + public IReadOnlyList<Colour4> TierColours { get => tierColours; set { - if (value.Length == 0 || value == tierColours) - return; - - tierCount = value.Length; + tierCount = value.Count; tierColours = value; graphNeedsUpdate = true; @@ -154,8 +152,6 @@ namespace osu.Game.Graphics.UserInterface segments.Sort(); } - private Colour4 getTierColour(int tier) => tier >= 0 ? tierColours[tier] : new Colour4(0, 0, 0, 0); - protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this); protected struct SegmentInfo @@ -203,6 +199,7 @@ namespace osu.Game.Graphics.UserInterface private IShader shader = null!; private readonly List<SegmentInfo> segments = new List<SegmentInfo>(); private Vector2 drawSize; + private readonly List<Colour4> tierColours = new List<Colour4>(); public SegmentedGraphDrawNode(SegmentedGraph<T> source) : base(source) @@ -216,8 +213,12 @@ namespace osu.Game.Graphics.UserInterface texture = Source.texture; shader = Source.shader; drawSize = Source.DrawSize; + segments.Clear(); segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1)); + + tierColours.Clear(); + tierColours.AddRange(Source.tierColours); } public override void Draw(IRenderer renderer) @@ -240,11 +241,27 @@ namespace osu.Game.Graphics.UserInterface Vector2Extensions.Transform(topRight, DrawInfo.Matrix), Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix), Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)), - Source.getTierColour(segment.Tier)); + getSegmentColour(segment)); } shader.Unbind(); } + + private ColourInfo getSegmentColour(SegmentInfo segment) + { + var segmentColour = new ColourInfo + { + TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)), + TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)), + BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)), + BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f)) + }; + + var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0); + segmentColour.ApplyChild(tierColour); + + return segmentColour; + } } protected class SegmentManager : IEnumerable<SegmentInfo> diff --git a/osu.Game/Graphics/UserInterface/StatefulMenuItem.cs b/osu.Game/Graphics/UserInterface/StatefulMenuItem.cs index 17266e876c..85efd75a60 100644 --- a/osu.Game/Graphics/UserInterface/StatefulMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/StatefulMenuItem.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -25,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface /// <param name="text">The text to display.</param> /// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param> /// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param> - protected StatefulMenuItem(string text, Func<object, object> changeStateFunc, MenuItemType type = MenuItemType.Standard) + protected StatefulMenuItem(LocalisableString text, Func<object, object> changeStateFunc, MenuItemType type = MenuItemType.Standard) : this(text, changeStateFunc, type, null) { } @@ -37,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface /// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param> /// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param> /// <param name="action">A delegate to be invoked when this <see cref="StatefulMenuItem"/> is pressed.</param> - protected StatefulMenuItem(string text, Func<object, object> changeStateFunc, MenuItemType type, Action<object> action) + protected StatefulMenuItem(LocalisableString text, Func<object, object>? changeStateFunc, MenuItemType type, Action<object>? action) : base(text, type) { Action.Value = () => @@ -69,7 +68,7 @@ namespace osu.Game.Graphics.UserInterface /// <param name="text">The text to display.</param> /// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param> /// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param> - protected StatefulMenuItem(string text, Func<T, T> changeStateFunc, MenuItemType type = MenuItemType.Standard) + protected StatefulMenuItem(LocalisableString text, Func<T, T>? changeStateFunc, MenuItemType type = MenuItemType.Standard) : this(text, changeStateFunc, type, null) { } @@ -81,7 +80,7 @@ namespace osu.Game.Graphics.UserInterface /// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="StatefulMenuItem"/> is pressed.</param> /// <param name="type">The type of action which this <see cref="StatefulMenuItem"/> performs.</param> /// <param name="action">A delegate to be invoked when this <see cref="StatefulMenuItem"/> is pressed.</param> - protected StatefulMenuItem(string text, Func<T, T> changeStateFunc, MenuItemType type, Action<T> action) + protected StatefulMenuItem(LocalisableString text, Func<T, T>? changeStateFunc, MenuItemType type, Action<T>? action) : base(text, o => changeStateFunc?.Invoke((T)o) ?? o, type, o => action?.Invoke((T)o)) { base.State.BindValueChanged(state => diff --git a/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs b/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs index 6787e17113..51e1248bc1 100644 --- a/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -18,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface /// </summary> /// <param name="text">The text to display.</param> /// <param name="type">The type of action which this <see cref="ToggleMenuItem"/> performs.</param> - public ToggleMenuItem(string text, MenuItemType type = MenuItemType.Standard) + public ToggleMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard) : this(text, type, null) { } @@ -29,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface /// <param name="text">The text to display.</param> /// <param name="type">The type of action which this <see cref="ToggleMenuItem"/> performs.</param> /// <param name="action">A delegate to be invoked when this <see cref="ToggleMenuItem"/> is pressed.</param> - public ToggleMenuItem(string text, MenuItemType type, Action<bool> action) + public ToggleMenuItem(LocalisableString text, MenuItemType type, Action<bool>? action) : base(text, value => !value, type, action) { } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index e66e48373c..d89322cecd 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -14,6 +15,7 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osuTK; +using osuTK.Input; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -58,6 +60,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 this.FadeOut(fade_duration, Easing.OutQuint); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key == Key.Escape) + return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back). + + return base.OnKeyDown(e); + } + public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) { if (e.Repeat) @@ -68,7 +78,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (e.Action == GlobalAction.Back) { - Hide(); + this.HidePopover(); return true; } diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 7bd284a94e..6b0a6bd8e1 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion"); + /// <summary> + /// "Mention" + /// </summary> + public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 385ebd0593..fed7b6cab7 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -104,6 +104,61 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"Description"); + /// <summary> + /// "File" + /// </summary> + public static LocalisableString MenuBarFile => new TranslatableString(getKey(@"menu_bar_file"), @"File"); + + /// <summary> + /// "Edit" + /// </summary> + public static LocalisableString MenuBarEdit => new TranslatableString(getKey(@"menu_bar_edit"), @"Edit"); + + /// <summary> + /// "View" + /// </summary> + public static LocalisableString MenuBarView => new TranslatableString(getKey(@"menu_bar_view"), @"View"); + + /// <summary> + /// "Undo" + /// </summary> + public static LocalisableString Undo => new TranslatableString(getKey(@"undo"), @"Undo"); + + /// <summary> + /// "Redo" + /// </summary> + public static LocalisableString Redo => new TranslatableString(getKey(@"redo"), @"Redo"); + + /// <summary> + /// "Cut" + /// </summary> + public static LocalisableString Cut => new TranslatableString(getKey(@"cut"), @"Cut"); + + /// <summary> + /// "Copy" + /// </summary> + public static LocalisableString Copy => new TranslatableString(getKey(@"copy"), @"Copy"); + + /// <summary> + /// "Paste" + /// </summary> + public static LocalisableString Paste => new TranslatableString(getKey(@"paste"), @"Paste"); + + /// <summary> + /// "Clone" + /// </summary> + public static LocalisableString Clone => new TranslatableString(getKey(@"clone"), @"Clone"); + + /// <summary> + /// "Exit" + /// </summary> + public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit"); + + /// <summary> + /// "Revert to default" + /// </summary> + public static LocalisableString RevertToDefault => new TranslatableString(getKey(@"revert_to_default"), @"Revert to default"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorDialogsStrings.cs b/osu.Game/Localisation/EditorDialogsStrings.cs new file mode 100644 index 0000000000..fc4c2b7f2a --- /dev/null +++ b/osu.Game/Localisation/EditorDialogsStrings.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorDialogsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorDialogs"; + + /// <summary> + /// "Would you like to create a blank difficulty?" + /// </summary> + public static LocalisableString NewDifficultyDialogHeader => new TranslatableString(getKey(@"new_difficulty_dialog_header"), @"Would you like to create a blank difficulty?"); + + /// <summary> + /// "Yeah, let's start from scratch!" + /// </summary> + public static LocalisableString CreateNew => new TranslatableString(getKey(@"create_new"), @"Yeah, let's start from scratch!"); + + /// <summary> + /// "No, create an exact copy of this difficulty" + /// </summary> + public static LocalisableString CreateCopy => new TranslatableString(getKey(@"create_copy"), @"No, create an exact copy of this difficulty"); + + /// <summary> + /// "I changed my mind, I want to keep editing this difficulty" + /// </summary> + public static LocalisableString KeepEditing => new TranslatableString(getKey(@"keep_editing"), @"I changed my mind, I want to keep editing this difficulty"); + + /// <summary> + /// "Did you want to save your changes?" + /// </summary> + public static LocalisableString SaveDialogHeader => new TranslatableString(getKey(@"save_dialog_header"), @"Did you want to save your changes?"); + + /// <summary> + /// "Save my masterpiece!" + /// </summary> + public static LocalisableString Save => new TranslatableString(getKey(@"save"), @"Save my masterpiece!"); + + /// <summary> + /// "Forget all changes" + /// </summary> + public static LocalisableString ForgetAllChanges => new TranslatableString(getKey(@"forget_all_changes"), @"Forget all changes"); + + /// <summary> + /// "Oops, continue editing" + /// </summary> + public static LocalisableString ContinueEditing => new TranslatableString(getKey(@"continue_editing"), @"Oops, continue editing"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 0431b9cf76..4ddacf2c5b 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -42,7 +42,8 @@ namespace osu.Game.Localisation /// <summary> /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." /// </summary> - public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); + public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), + @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); /// <summary> /// "Countdown speed" @@ -52,7 +53,8 @@ namespace osu.Game.Localisation /// <summary> /// "If the countdown sounds off-time, use this to make it appear one or more beats early." /// </summary> - public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); + public static LocalisableString CountdownOffsetDescription => + new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); /// <summary> /// "Countdown offset" @@ -67,7 +69,8 @@ namespace osu.Game.Localisation /// <summary> /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." /// </summary> - public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); + public static LocalisableString WidescreenSupportDescription => + new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); /// <summary> /// "Epilepsy warning" @@ -77,7 +80,8 @@ namespace osu.Game.Localisation /// <summary> /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." /// </summary> - public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); + public static LocalisableString EpilepsyWarningDescription => + new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); /// <summary> /// "Letterbox during breaks" @@ -87,7 +91,8 @@ namespace osu.Game.Localisation /// <summary> /// "Adds horizontal letterboxing to give a cinematic look during breaks." /// </summary> - public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); + public static LocalisableString LetterboxDuringBreaksDescription => + new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); /// <summary> /// "Samples match playback rate" @@ -97,7 +102,8 @@ namespace osu.Game.Localisation /// <summary> /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." /// </summary> - public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); + public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), + @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); /// <summary> /// "The size of all hit objects" @@ -117,7 +123,8 @@ namespace osu.Game.Localisation /// <summary> /// "The harshness of hit windows and difficulty of special objects (ie. spinners)" /// </summary> - public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); + public static LocalisableString OverallDifficultyDescription => + new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); /// <summary> /// "Metadata" @@ -199,6 +206,11 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString DifficultyHeader => new TranslatableString(getKey(@"difficulty_header"), @"Difficulty"); + /// <summary> + /// "Drag image here to set beatmap background!" + /// </summary> + public static LocalisableString DragToSetBackground => new TranslatableString(getKey(@"drag_to_set_background"), @"Drag image here to set beatmap background!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs new file mode 100644 index 0000000000..96c08aa6f8 --- /dev/null +++ b/osu.Game/Localisation/EditorStrings.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.Editor"; + + /// <summary> + /// "Waveform opacity" + /// </summary> + public static LocalisableString WaveformOpacity => new TranslatableString(getKey(@"waveform_opacity"), @"Waveform opacity"); + + /// <summary> + /// "Show hit markers" + /// </summary> + public static LocalisableString ShowHitMarkers => new TranslatableString(getKey(@"show_hit_markers"), @"Show hit markers"); + + /// <summary> + /// "Timing" + /// </summary> + public static LocalisableString Timing => new TranslatableString(getKey(@"timing"), @"Timing"); + + /// <summary> + /// "Set preview point to current time" + /// </summary> + public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time"); + + /// <summary> + /// "Export package" + /// </summary> + public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package"); + + /// <summary> + /// "Create new difficulty" + /// </summary> + public static LocalisableString CreateNewDifficulty => new TranslatableString(getKey(@"create_new_difficulty"), @"Create new difficulty"); + + /// <summary> + /// "Change difficulty" + /// </summary> + public static LocalisableString ChangeDifficulty => new TranslatableString(getKey(@"change_difficulty"), @"Change difficulty"); + + /// <summary> + /// "Delete difficulty" + /// </summary> + public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty"); + + /// <summary> + /// "setup" + /// </summary> + public static LocalisableString SetupScreen => new TranslatableString(getKey(@"setup_screen"), @"setup"); + + /// <summary> + /// "compose" + /// </summary> + public static LocalisableString ComposeScreen => new TranslatableString(getKey(@"compose_screen"), @"compose"); + + /// <summary> + /// "design" + /// </summary> + public static LocalisableString DesignScreen => new TranslatableString(getKey(@"design_screen"), @"design"); + + /// <summary> + /// "timing" + /// </summary> + public static LocalisableString TimingScreen => new TranslatableString(getKey(@"timing_screen"), @"timing"); + + /// <summary> + /// "verify" + /// </summary> + public static LocalisableString VerifyScreen => new TranslatableString(getKey(@"verify_screen"), @"verify"); + + /// <summary> + /// "Playback speed" + /// </summary> + public static LocalisableString PlaybackSpeed => new TranslatableString(getKey(@"playback_speed"), @"Playback speed"); + + /// <summary> + /// "Test!" + /// </summary> + public static LocalisableString TestBeatmap => new TranslatableString(getKey(@"test_beatmap"), @"Test!"); + + /// <summary> + /// "Waveform" + /// </summary> + public static LocalisableString TimelineWaveform => new TranslatableString(getKey(@"timeline_waveform"), @"Waveform"); + + /// <summary> + /// "Ticks" + /// </summary> + public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 14e0bbbced..303dbb6f46 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -155,9 +155,9 @@ namespace osu.Game.Localisation public static LocalisableString ToggleProfile => new TranslatableString(getKey(@"toggle_profile"), @"Toggle profile"); /// <summary> - /// "Pause gameplay" + /// "Pause / resume gameplay" /// </summary> - public static LocalisableString PauseGameplay => new TranslatableString(getKey(@"pause_gameplay"), @"Pause gameplay"); + public static LocalisableString PauseGameplay => new TranslatableString(getKey(@"pause_gameplay"), @"Pause / resume gameplay"); /// <summary> /// "Setup mode" diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index bc4be56c80..1b0df6ecf6 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -14,6 +14,26 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets"); + /// <summary> + /// "Snaking in sliders" + /// </summary> + public static LocalisableString SnakingInSliders => new TranslatableString(getKey(@"snaking_in_sliders"), @"Snaking in sliders"); + + /// <summary> + /// "Snaking out sliders" + /// </summary> + public static LocalisableString SnakingOutSliders => new TranslatableString(getKey(@"snaking_out_sliders"), @"Snaking out sliders"); + + /// <summary> + /// "Cursor trail" + /// </summary> + public static LocalisableString CursorTrail => new TranslatableString(getKey(@"cursor_trail"), @"Cursor trail"); + + /// <summary> + /// "Playfield border style" + /// </summary> + public static LocalisableString PlayfieldBorderStyle => new TranslatableString(getKey(@"playfield_border_style"), @"Playfield border style"); + /// <summary> /// "None" /// </summary> @@ -29,6 +49,36 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString BorderFull => new TranslatableString(getKey(@"full_borders"), @"Full"); + /// <summary> + /// "Scrolling direction" + /// </summary> + public static LocalisableString ScrollingDirection => new TranslatableString(getKey(@"scrolling_direction"), @"Scrolling direction"); + + /// <summary> + /// "Up" + /// </summary> + public static LocalisableString ScrollingDirectionUp => new TranslatableString(getKey(@"scrolling_up"), @"Up"); + + /// <summary> + /// "Down" + /// </summary> + public static LocalisableString ScrollingDirectionDown => new TranslatableString(getKey(@"scrolling_down"), @"Down"); + + /// <summary> + /// "Scroll speed" + /// </summary> + public static LocalisableString ScrollSpeed => new TranslatableString(getKey(@"scroll_speed"), @"Scroll speed"); + + /// <summary> + /// "Timing-based note colouring" + /// </summary> + public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring"); + + /// <summary> + /// "{0}ms (speed {1})" + /// </summary> + public static LocalisableString ScrollSpeedTooltip(double arg0, int arg1) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", arg0, arg1); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs new file mode 100644 index 0000000000..b2e5f3aeb6 --- /dev/null +++ b/osu.Game/Localisation/SkinEditorStrings.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class SkinEditorStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SkinEditor"; + + /// <summary> + /// "Skin editor" + /// </summary> + public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Skin editor"); + + /// <summary> + /// "Components" + /// </summary> + public static LocalisableString Components => new TranslatableString(getKey(@"components"), @"Components"); + + /// <summary> + /// "Scene library" + /// </summary> + public static LocalisableString SceneLibrary => new TranslatableString(getKey(@"scene_library"), @"Scene library"); + + /// <summary> + /// "Song Select" + /// </summary> + public static LocalisableString SongSelect => new TranslatableString(getKey(@"song_select"), @"Song Select"); + + /// <summary> + /// "Gameplay" + /// </summary> + public static LocalisableString Gameplay => new TranslatableString(getKey(@"gameplay"), @"Gameplay"); + + /// <summary> + /// "Settings ({0})" + /// </summary> + public static LocalisableString Settings(string arg0) => new TranslatableString(getKey(@"settings"), @"Settings ({0})", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/ToolbarStrings.cs b/osu.Game/Localisation/ToolbarStrings.cs index 6dc8a1e50c..e71a3fff9b 100644 --- a/osu.Game/Localisation/ToolbarStrings.cs +++ b/osu.Game/Localisation/ToolbarStrings.cs @@ -19,6 +19,21 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting..."); + /// <summary> + /// "home" + /// </summary> + public static LocalisableString HomeHeaderTitle => new TranslatableString(getKey(@"home_header_title"), @"home"); + + /// <summary> + /// "return to the main menu" + /// </summary> + public static LocalisableString HomeHeaderDescription => new TranslatableString(getKey(@"home_header_description"), @"return to the main menu"); + + /// <summary> + /// "play some {0}" + /// </summary> + public static LocalisableString PlaySomeRuleset(string arg0) => new TranslatableString(getKey(@"play_some_ruleset"), @"play some {0}", arg0); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/API/Requests/CommentPostRequest.cs b/osu.Game/Online/API/Requests/CommentPostRequest.cs new file mode 100644 index 0000000000..45e5eb1a94 --- /dev/null +++ b/osu.Game/Online/API/Requests/CommentPostRequest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class CommentPostRequest : APIRequest<CommentBundle> + { + public readonly CommentableType Commentable; + public readonly long CommentableId; + public readonly string Message; + public readonly long? ParentCommentId; + + public CommentPostRequest(CommentableType commentable, long commentableId, string message, long? parentCommentId = null) + { + Commentable = commentable; + CommentableId = commentableId; + Message = message; + ParentCommentId = parentCommentId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + + req.AddParameter(@"comment[commentable_type]", Commentable.ToString().ToLowerInvariant()); + req.AddParameter(@"comment[commentable_id]", $"{CommentableId}"); + req.AddParameter(@"comment[message]", Message); + if (ParentCommentId.HasValue) + req.AddParameter(@"comment[parent_id]", $"{ParentCommentId}"); + + return req; + } + + protected override string Target => "comments"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 27ad2a746c..7d6740ee46 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -63,6 +63,9 @@ namespace osu.Game.Online.API.Requests.Responses set => Length = TimeSpan.FromSeconds(value).TotalMilliseconds; } + [JsonProperty(@"convert")] + public bool Convert { get; set; } + [JsonProperty(@"count_circles")] public int CircleCount { get; set; } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index aeae3edde2..d98715a42d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -125,6 +125,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"beatmaps")] public APIBeatmap[] Beatmaps { get; set; } = Array.Empty<APIBeatmap>(); + [JsonProperty(@"converts")] + public APIBeatmap[]? Converts { get; set; } + private BeatmapMetadata metadata => new BeatmapMetadata { Title = Title, diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 0a5434822b..e3b5037367 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -25,6 +25,7 @@ namespace osu.Game.Online.Chat /// </summary> public partial class StandAloneChatDisplay : CompositeDrawable { + [Cached] public readonly Bindable<Channel> Channel = new Bindable<Channel>(); protected readonly ChatTextBox TextBox; diff --git a/osu.Game/Online/ExperimentalEndpointConfiguration.cs b/osu.Game/Online/ExperimentalEndpointConfiguration.cs new file mode 100644 index 0000000000..c3d0014c8b --- /dev/null +++ b/osu.Game/Online/ExperimentalEndpointConfiguration.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online +{ + public class ExperimentalEndpointConfiguration : EndpointConfiguration + { + public ExperimentalEndpointConfiguration() + { + WebsiteRootUrl = @"https://osu.ppy.sh"; + APIEndpointUrl = @"https://lazer.ppy.sh"; + APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; + APIClientID = "5"; + SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; + MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + } + } +} diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 003ec50afd..0244761b65 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -1,16 +1,13 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Online { public class ProductionEndpointConfiguration : EndpointConfiguration { public ProductionEndpointConfiguration() { - WebsiteRootUrl = @"https://osu.ppy.sh"; - APIEndpointUrl = @"https://lazer.ppy.sh"; + WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b27be37591..cf58d07b9e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -98,8 +98,8 @@ namespace osu.Game public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; - internal EndpointConfiguration CreateEndpoints() => - UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + public virtual EndpointConfiguration CreateEndpoints() => + UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ExperimentalEndpointConfiguration(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); @@ -160,9 +160,12 @@ namespace osu.Game protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method + /// <summary> + /// The current ruleset selection for the local user. + /// </summary> [Cached] [Cached(typeof(IBindable<RulesetInfo>))] - protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); + protected internal readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); /// <summary> /// The current mod selection for the local user. @@ -553,8 +556,8 @@ namespace osu.Game case JoystickHandler jh: return new JoystickSettings(jh); - case TouchHandler: - return new InputSection.HandlerSection(handler); + case TouchHandler th: + return new TouchSettings(th); } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index 34e0408db6..025738710f 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs @@ -17,18 +17,21 @@ namespace osu.Game.Overlays.BeatmapListing { public readonly Bindable<SortDirection> SortDirection = new Bindable<SortDirection>(Overlays.SortDirection.Descending); - private SearchCategory? lastCategory; - private bool? lastHasQuery; + private (SearchCategory category, bool hasQuery)? currentParameters; protected override void LoadComplete() { base.LoadComplete(); - Reset(SearchCategory.Leaderboard, false); + + if (currentParameters == null) + Reset(SearchCategory.Leaderboard, false); } public void Reset(SearchCategory category, bool hasQuery) { - if (category != lastCategory || hasQuery != lastHasQuery) + var newParameters = (category, hasQuery); + + if (currentParameters != newParameters) { TabControl.Clear(); @@ -63,8 +66,7 @@ namespace osu.Game.Overlays.BeatmapListing // see: https://github.com/ppy/osu-framework/issues/5412 TabControl.Current.TriggerChange(); - lastCategory = category; - lastHasQuery = hasQuery; + currentParameters = newParameters; } protected override SortTabControl CreateControl() => new BeatmapSortTabControl diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 84d12f2611..585e0dd1a2 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework; @@ -38,10 +36,10 @@ namespace osu.Game.Overlays.BeatmapSet public readonly DifficultiesContainer Difficulties; - public readonly Bindable<APIBeatmap> Beatmap = new Bindable<APIBeatmap>(); - private APIBeatmapSet beatmapSet; + public readonly Bindable<APIBeatmap?> Beatmap = new Bindable<APIBeatmap?>(); + private APIBeatmapSet? beatmapSet; - public APIBeatmapSet BeatmapSet + public APIBeatmapSet? BeatmapSet { get => beatmapSet; set @@ -142,7 +140,7 @@ namespace osu.Game.Overlays.BeatmapSet } [Resolved] - private IBindable<RulesetInfo> ruleset { get; set; } + private IBindable<RulesetInfo> ruleset { get; set; } = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -168,10 +166,11 @@ namespace osu.Game.Overlays.BeatmapSet if (BeatmapSet != null) { - Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps + Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Concat(BeatmapSet.Converts ?? Array.Empty<APIBeatmap>()) .Where(b => b.Ruleset.MatchesOnlineID(ruleset.Value)) - .OrderBy(b => b.StarRating) - .Select(b => new DifficultySelectorButton(b) + .OrderBy(b => !b.Convert) + .ThenBy(b => b.StarRating) + .Select(b => new DifficultySelectorButton(b, b.Convert ? new RulesetInfo { OnlineID = 0 } : null) { State = DifficultySelectorState.NotSelected, OnHovered = beatmap => @@ -199,9 +198,9 @@ namespace osu.Game.Overlays.BeatmapSet updateDifficultyButtons(); } - private void showBeatmap(IBeatmapInfo beatmapInfo) + private void showBeatmap(IBeatmapInfo? beatmapInfo) { - version.Text = beatmapInfo?.DifficultyName; + version.Text = beatmapInfo?.DifficultyName ?? string.Empty; } private void updateDifficultyButtons() @@ -211,7 +210,7 @@ namespace osu.Game.Overlays.BeatmapSet public partial class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton> { - public Action OnLostHover; + public Action? OnLostHover; protected override void OnHoverLost(HoverLostEvent e) { @@ -232,9 +231,9 @@ namespace osu.Game.Overlays.BeatmapSet public readonly APIBeatmap Beatmap; - public Action<APIBeatmap> OnHovered; - public Action<APIBeatmap> OnClicked; - public event Action<DifficultySelectorState> StateChanged; + public Action<APIBeatmap>? OnHovered; + public Action<APIBeatmap>? OnClicked; + public event Action<DifficultySelectorState>? StateChanged; private DifficultySelectorState state; @@ -255,7 +254,7 @@ namespace osu.Game.Overlays.BeatmapSet } } - public DifficultySelectorButton(APIBeatmap beatmapInfo) + public DifficultySelectorButton(APIBeatmap beatmapInfo, IRulesetInfo? ruleset) { Beatmap = beatmapInfo; Size = new Vector2(size); @@ -274,7 +273,7 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0.5f } }, - icon = new DifficultyIcon(beatmapInfo) + icon = new DifficultyIcon(beatmapInfo, ruleset) { ShowTooltip = false, Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) }, diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs index f802807c3c..76e2f256b0 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapRulesetTabItem.cs @@ -68,11 +68,12 @@ namespace osu.Game.Overlays.BeatmapSet BeatmapSet.BindValueChanged(setInfo => { int beatmapsCount = setInfo.NewValue?.Beatmaps.Count(b => b.Ruleset.MatchesOnlineID(Value)) ?? 0; + int osuBeatmaps = setInfo.NewValue?.Beatmaps.Count(b => b.Ruleset.OnlineID == 0) ?? 0; count.Text = beatmapsCount.ToString(); countContainer.FadeTo(beatmapsCount > 0 ? 1 : 0); - Enabled.Value = beatmapsCount > 0; + Enabled.Value = beatmapsCount > 0 || osuBeatmaps > 0; }, true); } } diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 671d649dcf..4cc38c41e4 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -5,12 +5,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Online.API.Requests; @@ -22,8 +24,6 @@ namespace osu.Game.Overlays { public partial class ChangelogOverlay : OnlineOverlay<ChangelogHeader> { - public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; - public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>(); private List<APIChangelogBuild> builds; @@ -81,6 +81,8 @@ namespace osu.Game.Overlays ArgumentNullException.ThrowIfNull(updateStream); ArgumentNullException.ThrowIfNull(version); + Show(); + performAfterFetch(() => { var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream) @@ -89,8 +91,6 @@ namespace osu.Game.Overlays if (build != null) ShowBuild(build); }); - - Show(); } public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) @@ -127,11 +127,16 @@ namespace osu.Game.Overlays private Task initialFetchTask; - private void performAfterFetch(Action action) => Schedule(() => + private void performAfterFetch(Action action) { - fetchListing()?.ContinueWith(_ => - Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); - }); + Debug.Assert(State.Value == Visibility.Visible); + + Schedule(() => + { + fetchListing()?.ContinueWith(_ => + Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); + }); + } private Task fetchListing() { diff --git a/osu.Game/Overlays/Chat/DrawableUsername.cs b/osu.Game/Overlays/Chat/DrawableUsername.cs index 8cd16047f3..031a0b6ae2 100644 --- a/osu.Game/Overlays/Chat/DrawableUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableUsername.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -24,6 +25,7 @@ using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; +using ChatStrings = osu.Game.Localisation.ChatStrings; namespace osu.Game.Overlays.Chat { @@ -65,6 +67,9 @@ namespace osu.Game.Overlays.Chat [Resolved(canBeNull: true)] private UserProfileOverlay? profileOverlay { get; set; } + [Resolved] + private Bindable<Channel?>? currentChannel { get; set; } + private readonly APIUser user; private readonly OsuSpriteText drawableText; @@ -156,6 +161,14 @@ namespace osu.Game.Overlays.Chat if (!user.Equals(api.LocalUser.Value)) items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + if (currentChannel?.Value != null) + { + items.Add(new OsuMenuItem(ChatStrings.MentionUser, MenuItemType.Standard, () => + { + currentChannel.Value.TextBoxMessage.Value += $"@{user.Username} "; + })); + } + return items.ToArray(); } } diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index aa9b2df7e4..555823e996 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Humanizer; using osu.Framework.Bindables; using osu.Framework.Input.Events; @@ -15,7 +13,12 @@ namespace osu.Game.Overlays.Comments.Buttons public ShowRepliesButton(int count) { - Text = "reply".ToQuantity(count); + Count = count; + } + + public int Count + { + set => Text = "reply".ToQuantity(value); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 769263b3fc..2af7dd3093 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -35,6 +35,8 @@ namespace osu.Game.Overlays.Comments private RoundedButton commitButton = null!; private LoadingSpinner loadingSpinner = null!; + protected TextBox TextBox { get; private set; } = null!; + protected bool ShowLoadingSpinner { set @@ -51,8 +53,6 @@ namespace osu.Game.Overlays.Comments [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - EditorTextBox textBox; - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Comments Direction = FillDirection.Vertical, Children = new Drawable[] { - textBox = new EditorTextBox + TextBox = new EditorTextBox { Height = 40, RelativeSizeAxes = Axes.X, @@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Comments } }); - textBox.OnCommit += (_, _) => commitButton.TriggerClick(); + TextBox.OnCommit += (_, _) => commitButton.TriggerClick(); } protected override void LoadComplete() @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Comments private void updateCommitButtonState() => commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); - private partial class EditorTextBox : BasicTextBox + private partial class EditorTextBox : OsuTextBox { protected override float LeftRightPadding => side_padding; @@ -173,12 +173,6 @@ namespace osu.Game.Overlays.Comments { Font = OsuFont.GetFont(weight: FontWeight.Regular), }; - - protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer - { - AutoSizeAxes = Axes.Both, - Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) } - }; } protected partial class EditorButton : RoundedButton diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 7bd2d6a5e6..c4e4700674 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Online.API; @@ -17,16 +18,22 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Threading; using System.Collections.Generic; using JetBrains.Annotations; +using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Overlays.Comments { + [Cached] public partial class CommentsContainer : CompositeDrawable { private readonly Bindable<CommentableType> type = new Bindable<CommentableType>(); private readonly BindableLong id = new BindableLong(); + public IBindable<CommentableType> Type => type; + public IBindable<long> Id => id; public readonly Bindable<CommentsSortCriteria> Sort = new Bindable<CommentsSortCriteria>(); public readonly BindableBool ShowDeleted = new BindableBool(); @@ -46,12 +53,14 @@ namespace osu.Game.Overlays.Comments private DeletedCommentsCounter deletedCommentsCounter; private CommentsShowMoreButton moreButton; private TotalCommentsCounter commentCounter; + private UpdateableAvatar avatar; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + AddRangeInternal(new Drawable[] { new Box @@ -86,6 +95,32 @@ namespace osu.Game.Overlays.Comments }, }, }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50, Vertical = 20 }, + Children = new Drawable[] + { + avatar = new UpdateableAvatar(api.LocalUser.Value) + { + Size = new Vector2(50), + CornerExponent = 2, + CornerRadius = 25, + Masking = true, + }, + new Container + { + Padding = new MarginPadding { Left = 60 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new NewCommentEditor + { + OnPost = prependPostedComments + } + } + } + }, new CommentsHeader { Sort = { BindTarget = Sort }, @@ -151,6 +186,7 @@ namespace osu.Game.Overlays.Comments protected override void LoadComplete() { User.BindValueChanged(_ => refetchComments()); + User.BindValueChanged(e => avatar.User = e.NewValue); Sort.BindValueChanged(_ => refetchComments(), true); base.LoadComplete(); } @@ -245,7 +281,6 @@ namespace osu.Game.Overlays.Comments { pinnedContent.AddRange(loaded.Where(d => d.Comment.Pinned)); content.AddRange(loaded.Where(d => !d.Comment.Pinned)); - deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel); if (bundle.HasMore) @@ -266,7 +301,7 @@ namespace osu.Game.Overlays.Comments void addNewComment(Comment comment) { - var drawableComment = getDrawableComment(comment); + var drawableComment = GetDrawableComment(comment); if (comment.ParentId == null) { @@ -288,7 +323,35 @@ namespace osu.Game.Overlays.Comments } } - private DrawableComment getDrawableComment(Comment comment) + private void prependPostedComments(CommentBundle bundle) + { + var topLevelComments = new List<DrawableComment>(); + + foreach (var comment in bundle.Comments) + { + // Exclude possible duplicated comments. + if (CommentDictionary.ContainsKey(comment.Id)) + continue; + + topLevelComments.Add(GetDrawableComment(comment)); + } + + if (topLevelComments.Any()) + { + LoadComponentsAsync(topLevelComments, loaded => + { + if (content.Count > 0 && content[0] is NoCommentsPlaceholder placeholder) + content.Remove(placeholder, true); + + foreach (var comment in loaded) + { + content.Insert((int)-Clock.CurrentTime, comment); + } + }, (loadCancellation = new CancellationTokenSource()).Token); + } + } + + public DrawableComment GetDrawableComment(Comment comment) { if (CommentDictionary.TryGetValue(comment.Id, out var existing)) return existing; @@ -317,7 +380,7 @@ namespace osu.Game.Overlays.Comments base.Dispose(isDisposing); } - private partial class NoCommentsPlaceholder : CompositeDrawable + internal partial class NoCommentsPlaceholder : CompositeDrawable { [BackgroundDependencyLoader] private void load() @@ -336,5 +399,41 @@ namespace osu.Game.Overlays.Comments }); } } + + private partial class NewCommentEditor : CommentEditor + { + [Resolved] + private CommentsContainer commentsContainer { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + public Action<CommentBundle> OnPost; + + //TODO should match web, left empty due to no multiline support + protected override LocalisableString FooterText => default; + + protected override LocalisableString CommitButtonText => CommonStrings.ButtonsPost; + + protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderNew; + + protected override void OnCommit(string text) + { + ShowLoadingSpinner = true; + CommentPostRequest req = new CommentPostRequest(commentsContainer.Type.Value, commentsContainer.Id.Value, text); + req.Failure += e => Schedule(() => + { + ShowLoadingSpinner = false; + Logger.Error(e, "Posting comment failed."); + }); + req.Success += cb => Schedule(() => + { + ShowLoadingSpinner = false; + Current.Value = string.Empty; + OnPost?.Invoke(cb); + }); + api.Queue(req); + } + } } } diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 834abd5e9f..4cc189f3a2 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -22,6 +22,7 @@ using System.Collections.Specialized; using System.Diagnostics; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -57,6 +58,11 @@ namespace osu.Game.Overlays.Comments /// </summary> public bool WasDeleted { get; protected set; } + /// <summary> + /// Tracks this comment's level of nesting. 0 means that this comment has no parents. + /// </summary> + public int Level { get; private set; } + private FillFlowContainer childCommentsVisibilityContainer = null!; private FillFlowContainer childCommentsContainer = null!; private LoadRepliesButton loadRepliesButton = null!; @@ -69,8 +75,9 @@ namespace osu.Game.Overlays.Comments private OsuSpriteText deletedLabel = null!; private GridContainer content = null!; private VotePill votePill = null!; + private Container<CommentEditor> replyEditorContainer = null!; - [Resolved(canBeNull: true)] + [Resolved] private IDialogOverlay? dialogOverlay { get; set; } [Resolved] @@ -79,7 +86,7 @@ namespace osu.Game.Overlays.Comments [Resolved] private GameHost host { get; set; } = null!; - [Resolved(canBeNull: true)] + [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } public DrawableComment(Comment comment) @@ -88,12 +95,16 @@ namespace osu.Game.Overlays.Comments } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, DrawableComment? parentComment) { LinkFlowContainer username; FillFlowContainer info; CommentMarkdownContainer message; + Level = parentComment?.Level + 1 ?? 0; + + float childrenPadding = Level < 6 ? 20 : 5; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] @@ -223,6 +234,12 @@ namespace osu.Game.Overlays.Comments } } }, + replyEditorContainer = new Container<CommentEditor> + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Top = 10 }, + }, new Container { AutoSizeAxes = Axes.Both, @@ -245,10 +262,11 @@ namespace osu.Game.Overlays.Comments }, childCommentsVisibilityContainer = new FillFlowContainer { + Name = @"Children comments", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = 20 }, + Padding = new MarginPadding { Left = childrenPadding }, Children = new Drawable[] { childCommentsContainer = new FillFlowContainer @@ -335,6 +353,8 @@ namespace osu.Game.Overlays.Comments actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl); actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); + actionsContainer.AddLink(CommonStrings.ButtonsReply.ToLower(), toggleReply); + actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id) actionsContainer.AddLink(CommonStrings.ButtonsDelete.ToLower(), deleteComment); @@ -410,8 +430,9 @@ namespace osu.Game.Overlays.Comments if (!ShowDeleted.Value) Hide(); }); - request.Failure += _ => Schedule(() => + request.Failure += e => Schedule(() => { + Logger.Error(e, "Failed to delete comment"); actionsLoading.Hide(); actionsContainer.Show(); }); @@ -424,6 +445,26 @@ namespace osu.Game.Overlays.Comments onScreenDisplay?.Display(new CopyUrlToast()); } + private void toggleReply() + { + if (replyEditorContainer.Count == 0) + { + replyEditorContainer.Add(new ReplyCommentEditor(Comment) + { + OnPost = comments => + { + Comment.RepliesCount += comments.Length; + showRepliesButton.Count = Comment.RepliesCount; + Replies.AddRange(comments); + } + }); + } + else + { + replyEditorContainer.Clear(true); + } + } + protected override void LoadComplete() { ShowDeleted.BindValueChanged(show => @@ -436,8 +477,6 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); } - public bool ContainsReply(long replyId) => loadedReplies.ContainsKey(replyId); - private void onRepliesAdded(IEnumerable<DrawableComment> replies) { var page = createRepliesPage(replies); diff --git a/osu.Game/Overlays/Comments/ReplyCommentEditor.cs b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs new file mode 100644 index 0000000000..e738e6e7ec --- /dev/null +++ b/osu.Game/Overlays/Comments/ReplyCommentEditor.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Overlays.Comments +{ + public partial class ReplyCommentEditor : CancellableCommentEditor + { + [Resolved] + private CommentsContainer commentsContainer { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private readonly Comment parentComment; + + public Action<DrawableComment[]>? OnPost; + + protected override LocalisableString FooterText => default; + protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply; + protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply; + + public ReplyCommentEditor(Comment parent) + { + parentComment = parent; + OnCancel = () => this.FadeOut(200).Expire(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + GetContainingInputManager().ChangeFocus(TextBox); + } + + protected override void OnCommit(string text) + { + ShowLoadingSpinner = true; + CommentPostRequest req = new CommentPostRequest(commentsContainer.Type.Value, commentsContainer.Id.Value, text, parentComment.Id); + req.Failure += e => Schedule(() => + { + ShowLoadingSpinner = false; + Logger.Error(e, "Posting reply comment failed."); + }); + req.Success += cb => Schedule(processPostedComments, cb); + api.Queue(req); + } + + private void processPostedComments(CommentBundle cb) + { + foreach (var comment in cb.Comments) + comment.ParentComment = parentComment; + + var drawables = cb.Comments.Select(commentsContainer.GetDrawableComment).ToArray(); + OnPost?.Invoke(drawables); + + OnCancel!.Invoke(); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs similarity index 98% rename from osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs rename to osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs index be160a6948..508041eb76 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BadgeHeaderContainer.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Overlays.Profile.Header { - public partial class MedalHeaderContainer : CompositeDrawable + public partial class BadgeHeaderContainer : CompositeDrawable { private FillFlowContainer badgeFlowContainer = null!; diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 7721342ba8..0dab4d582d 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; using osuTK; @@ -15,6 +16,8 @@ namespace osu.Game.Overlays.Profile.Header { public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); + private LevelBadge levelBadge = null!; + public CentreHeaderContainer() { Height = 60; @@ -62,12 +65,11 @@ namespace osu.Game.Overlays.Profile.Header Margin = new MarginPadding { Right = UserProfileOverlay.CONTENT_X_MARGIN }, Children = new Drawable[] { - new LevelBadge + levelBadge = new LevelBadge { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Size = new Vector2(40), - User = { BindTarget = User } + Size = new Vector2(40) }, new Container { @@ -86,5 +88,17 @@ namespace osu.Game.Overlays.Profile.Header } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(user => updateDisplay(user.NewValue?.User), true); + } + + private void updateDisplay(APIUser? user) + { + levelBadge.LevelInfo.Value = user?.Statistics?.Level; + } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/ExtendedDetails.cs b/osu.Game/Overlays/Profile/Header/Components/ExtendedDetails.cs new file mode 100644 index 0000000000..50fc52600c --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/ExtendedDetails.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class ExtendedDetails : CompositeDrawable + { + public Bindable<UserProfileData?> User { get; } = new Bindable<UserProfileData?>(); + + private SpriteText rankedScore = null!; + private SpriteText hitAccuracy = null!; + private SpriteText playCount = null!; + private SpriteText totalScore = null!; + private SpriteText totalHits = null!; + private SpriteText maximumCombo = null!; + private SpriteText replaysWatched = null!; + + [BackgroundDependencyLoader] + private void load() + { + var font = OsuFont.Default.With(size: 12); + const float vertical_spacing = 4; + + AutoSizeAxes = Axes.Both; + + // this should really be a grid, but trying to avoid one to avoid the performance hit. + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20, 0), + Children = new[] + { + new FillFlowContainer + { + Name = @"Labels", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, vertical_spacing), + Children = new Drawable[] + { + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsRankedScore }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsHitAccuracy }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsPlayCount }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsTotalScore }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsTotalHits }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsMaximumCombo }, + new OsuSpriteText { Font = font, Text = UsersStrings.ShowStatsReplaysWatchedByOthers }, + } + }, + new FillFlowContainer + { + Name = @"Values", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, vertical_spacing), + Children = new Drawable[] + { + rankedScore = new OsuSpriteText { Font = font }, + hitAccuracy = new OsuSpriteText { Font = font }, + playCount = new OsuSpriteText { Font = font }, + totalScore = new OsuSpriteText { Font = font }, + totalHits = new OsuSpriteText { Font = font }, + maximumCombo = new OsuSpriteText { Font = font }, + replaysWatched = new OsuSpriteText { Font = font }, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(user => updateStatistics(user.NewValue?.User.Statistics), true); + } + + private void updateStatistics(UserStatistics? statistics) + { + if (statistics == null) + { + Alpha = 0; + return; + } + + Alpha = 1; + + rankedScore.Text = statistics.RankedScore.ToLocalisableString(@"N0"); + hitAccuracy.Text = statistics.DisplayAccuracy; + playCount.Text = statistics.PlayCount.ToLocalisableString(@"N0"); + totalScore.Text = statistics.TotalScore.ToLocalisableString(@"N0"); + totalHits.Text = statistics.TotalHits.ToLocalisableString(@"N0"); + maximumCombo.Text = statistics.MaxCombo.ToLocalisableString(@"N0"); + replaysWatched.Text = statistics.ReplaysWatched.ToLocalisableString(@"N0"); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs b/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs index e62f1cebf0..4d6ee36254 100644 --- a/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/GroupBadge.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public partial class GroupBadge : Container, IHasTooltip { - public LocalisableString TooltipText { get; } + public LocalisableString TooltipText { get; private set; } public int TextSize { get; set; } = 12; @@ -78,6 +78,11 @@ namespace osu.Game.Overlays.Profile.Header.Components icon.Size = new Vector2(TextSize - 1); })).ToList() ); + + var badgeModesList = group.Playmodes.Select(p => rulesets.GetRuleset(p)?.Name).ToList(); + + string modesDisplay = string.Join(", ", badgeModesList); + TooltipText += $" ({modesDisplay})"; } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs index 78c5231736..9b4df7672d 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; @@ -11,18 +12,23 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components { public partial class LevelBadge : CompositeDrawable, IHasTooltip { - public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); + public readonly Bindable<UserStatistics.LevelInfo?> LevelInfo = new Bindable<UserStatistics.LevelInfo?>(); public LocalisableString TooltipText { get; private set; } private OsuSpriteText levelText = null!; + private Sprite sprite = null!; + + [Resolved] + private OsuColour osuColour { get; set; } = null!; public LevelBadge() { @@ -34,7 +40,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { InternalChildren = new Drawable[] { - new Sprite + sprite = new Sprite { RelativeSizeAxes = Axes.Both, Texture = textures.Get("Profile/levelbadge"), @@ -47,15 +53,45 @@ namespace osu.Game.Overlays.Profile.Header.Components Font = OsuFont.GetFont(size: 20) } }; - - User.BindValueChanged(user => updateLevel(user.NewValue?.User)); } - private void updateLevel(APIUser? user) + protected override void LoadComplete() { - string level = user?.Statistics?.Level.Current.ToString() ?? "0"; - levelText.Text = level; - TooltipText = UsersStrings.ShowStatsLevel(level); + base.LoadComplete(); + + LevelInfo.BindValueChanged(level => updateLevel(level.NewValue), true); + } + + private void updateLevel(UserStatistics.LevelInfo? levelInfo) + { + int level = levelInfo?.Current ?? 0; + + levelText.Text = level.ToString(); + TooltipText = UsersStrings.ShowStatsLevel(level.ToString()); + + sprite.Colour = mapLevelToTierColour(level); + } + + private ColourInfo mapLevelToTierColour(int level) + { + var tier = RankingTier.Iron; + + if (level > 0) + { + tier = (RankingTier)(level / 20); + } + + if (level >= 105) + { + tier = RankingTier.Radiant; + } + + if (level >= 110) + { + tier = RankingTier.Lustrous; + } + + return osuColour.ForRankingTier(tier); } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs new file mode 100644 index 0000000000..b89973c5e5 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -0,0 +1,186 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public partial class MainDetails : CompositeDrawable + { + private readonly Dictionary<ScoreRank, ScoreRankInfo> scoreRankInfos = new Dictionary<ScoreRank, ScoreRankInfo>(); + private ProfileValueDisplay medalInfo = null!; + private ProfileValueDisplay ppInfo = null!; + private ProfileValueDisplay detailGlobalRank = null!; + private ProfileValueDisplay detailCountryRank = null!; + private RankGraph rankGraph = null!; + + public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + AutoSizeDuration = 200, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 15), + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new Drawable[] + { + detailGlobalRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankGlobalSimple, + }, + detailCountryRank = new ProfileValueDisplay(true) + { + Title = UsersStrings.ShowRankCountrySimple, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 60, + Children = new Drawable[] + { + rankGraph = new RankGraph + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + medalInfo = new ProfileValueDisplay + { + Title = UsersStrings.ShowStatsMedals, + }, + ppInfo = new ProfileValueDisplay + { + Title = "pp", + }, + new TotalPlayTime + { + User = { BindTarget = User } + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new[] + { + scoreRankInfos[ScoreRank.XH] = new ScoreRankInfo(ScoreRank.XH), + scoreRankInfos[ScoreRank.X] = new ScoreRankInfo(ScoreRank.X), + scoreRankInfos[ScoreRank.SH] = new ScoreRankInfo(ScoreRank.SH), + scoreRankInfos[ScoreRank.S] = new ScoreRankInfo(ScoreRank.S), + scoreRankInfos[ScoreRank.A] = new ScoreRankInfo(ScoreRank.A), + } + } + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(e => updateDisplay(e.NewValue), true); + } + + private void updateDisplay(UserProfileData? data) + { + var user = data?.User; + + medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; + ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; + + foreach (var scoreRankInfo in scoreRankInfos) + scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; + + detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + + rankGraph.Statistics.Value = user?.Statistics; + } + + private partial class ScoreRankInfo : CompositeDrawable + { + private readonly OsuSpriteText rankCount; + + public int RankCount + { + set => rankCount.Text = value.ToLocalisableString("#,##0"); + } + + public ScoreRankInfo(ScoreRank rank) + { + AutoSizeAxes = Axes.Both; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Width = 44, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DrawableRank(rank) + { + RelativeSizeAxes = Axes.X, + Height = 22, + }, + rankCount = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + } + }; + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs similarity index 68% rename from osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs rename to osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs index e7a83c95d8..9171d5de7d 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ExpandDetailsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ToggleCoverButton.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -15,11 +14,11 @@ using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { - public partial class ExpandDetailsButton : ProfileHeaderButton + public partial class ToggleCoverButton : ProfileHeaderButton { - public readonly BindableBool DetailsVisible = new BindableBool(); + public readonly BindableBool CoverExpanded = new BindableBool(true); - public override LocalisableString TooltipText => DetailsVisible.Value ? CommonStrings.ButtonsCollapse : CommonStrings.ButtonsExpand; + public override LocalisableString TooltipText => CoverExpanded.Value ? UsersStrings.ShowCoverTo0 : UsersStrings.ShowCoverTo1; private SpriteIcon icon = null!; private Sample? sampleOpen; @@ -27,12 +26,12 @@ namespace osu.Game.Overlays.Profile.Header.Components protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(); - public ExpandDetailsButton() + public ToggleCoverButton() { Action = () => { - DetailsVisible.Toggle(); - (DetailsVisible.Value ? sampleOpen : sampleClose)?.Play(); + CoverExpanded.Toggle(); + (CoverExpanded.Value ? sampleOpen : sampleClose)?.Play(); }; } @@ -40,19 +39,21 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load(OverlayColourProvider colourProvider, AudioManager audio) { IdleColour = colourProvider.Background2; - HoverColour = colourProvider.Background2.Lighten(0.2f); + HoverColour = colourProvider.Background1; sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + AutoSizeAxes = Axes.None; + Size = new Vector2(30); Child = icon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(20, 12) + Size = new Vector2(10.5f, 12) }; - DetailsVisible.BindValueChanged(visible => updateState(visible.NewValue), true); + CoverExpanded.BindValueChanged(visible => updateState(visible.NewValue), true); } private void updateState(bool detailsVisible) => icon.Icon = detailsVisible ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown; diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index 890e6c4e04..1cc3aae735 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -1,33 +1,17 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Leaderboards; using osu.Game.Overlays.Profile.Header.Components; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Scoring; -using osuTK; namespace osu.Game.Overlays.Profile.Header { public partial class DetailHeaderContainer : CompositeDrawable { - private readonly Dictionary<ScoreRank, ScoreRankInfo> scoreRankInfos = new Dictionary<ScoreRank, ScoreRankInfo>(); - private ProfileValueDisplay medalInfo = null!; - private ProfileValueDisplay ppInfo = null!; - private ProfileValueDisplay detailGlobalRank = null!; - private ProfileValueDisplay detailCountryRank = null!; - private RankGraph rankGraph = null!; - public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); [BackgroundDependencyLoader] @@ -35,8 +19,6 @@ namespace osu.Game.Overlays.Profile.Header { AutoSizeAxes = Axes.Y; - User.ValueChanged += e => updateDisplay(e.NewValue); - InternalChildren = new Drawable[] { new Box @@ -44,149 +26,52 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, }, - new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - AutoSizeDuration = 200, - AutoSizeEasing = Easing.OutQuint, - Masking = true, Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 15), - Children = new Drawable[] + Child = new GridContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new Drawable[] - { - detailGlobalRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankGlobalSimple, - }, - detailCountryRank = new ProfileValueDisplay(true) - { - Title = UsersStrings.ShowRankCountrySimple, - }, - } + new Dimension(GridSizeMode.AutoSize), }, - new Container + ColumnDimensions = new[] { - RelativeSizeAxes = Axes.X, - Height = 60, - Children = new Drawable[] - { - rankGraph = new RankGraph - { - RelativeSizeAxes = Axes.Both, - }, - } + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), }, - new Container + Content = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Drawable[] { - new FillFlowContainer + new MainDetails + { + RelativeSizeAxes = Axes.X, + User = { BindTarget = User } + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = 2, + Colour = colourProvider.Background6, + Margin = new MarginPadding { Horizontal = 15 } + }, + new ExtendedDetails { - AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - medalInfo = new ProfileValueDisplay - { - Title = UsersStrings.ShowStatsMedals, - }, - ppInfo = new ProfileValueDisplay - { - Title = "pp", - }, - new TotalPlayTime - { - User = { BindTarget = User } - }, - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new[] - { - scoreRankInfos[ScoreRank.XH] = new ScoreRankInfo(ScoreRank.XH), - scoreRankInfos[ScoreRank.X] = new ScoreRankInfo(ScoreRank.X), - scoreRankInfos[ScoreRank.SH] = new ScoreRankInfo(ScoreRank.SH), - scoreRankInfos[ScoreRank.S] = new ScoreRankInfo(ScoreRank.S), - scoreRankInfos[ScoreRank.A] = new ScoreRankInfo(ScoreRank.A), - } + User = { BindTarget = User } } } - }, - } - }, - }; - } - - private void updateDisplay(UserProfileData? data) - { - var user = data?.User; - - medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0"; - ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0"; - - foreach (var scoreRankInfo in scoreRankInfos) - scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; - - detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - - rankGraph.Statistics.Value = user?.Statistics; - } - - private partial class ScoreRankInfo : CompositeDrawable - { - private readonly OsuSpriteText rankCount; - - public int RankCount - { - set => rankCount.Text = value.ToLocalisableString("#,##0"); - } - - public ScoreRankInfo(ScoreRank rank) - { - AutoSizeAxes = Axes.Both; - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - Width = 44, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new DrawableRank(rank) - { - RelativeSizeAxes = Axes.X, - Height = 22, - }, - rankCount = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre } } - }; - } + } + }; } } } diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index e04a8ad9ad..652ebcec26 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -5,18 +5,18 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Profile.Header.Components; -using osu.Game.Resources.Localisation.Web; +using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -24,13 +24,15 @@ namespace osu.Game.Overlays.Profile.Header { public partial class TopHeaderContainer : CompositeDrawable { - private const float avatar_size = 110; + private const float content_height = 65; + private const float vertical_padding = 10; public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); [Resolved] private IAPIProvider api { get; set; } = null!; + private UserCoverBackground cover = null!; private SupporterIcon supporterTag = null!; private UpdateableAvatar avatar = null!; private OsuSpriteText usernameText = null!; @@ -38,13 +40,20 @@ namespace osu.Game.Overlays.Profile.Header private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; private OsuSpriteText userCountryText = null!; - private FillFlowContainer userStats = null!; private GroupBadgeFlow groupBadgeFlow = null!; + private ToggleCoverButton coverToggle = null!; + + private Bindable<bool> coverExpanded = null!; + + private FillFlowContainer flow = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuConfigManager configManager) { - Height = 150; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + coverExpanded = configManager.GetBindable<bool>(OsuSetting.ProfileCoverExpanded); InternalChildren = new Drawable[] { @@ -55,134 +64,147 @@ namespace osu.Game.Overlays.Profile.Header }, new FillFlowContainer { - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }, - Height = avatar_size, - AutoSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, Children = new Drawable[] { - avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false) + cover = new ProfileCoverBackground { - Size = new Vector2(avatar_size), - Masking = true, - CornerRadius = avatar_size * 0.25f, + RelativeSizeAxes = Axes.X, }, - new OsuContextMenuContainer + new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Child = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Padding = new MarginPadding { Left = 10 }, - Children = new Drawable[] + flow = new FillFlowContainer { - new FillFlowContainer + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - usernameText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular) - }, - openUserExternally = new ExternalLinkButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - groupBadgeFlow = new GroupBadgeFlow - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - } - }, - titleText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular) - }, - } + Left = UserProfileOverlay.CONTENT_X_MARGIN, + Vertical = vertical_padding }, - new FillFlowContainer + Height = content_height + 2 * vertical_padding, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false) { - supporterTag = new SupporterIcon + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Masking = true, + EdgeEffect = new EdgeEffectParameters { - Height = 20, - Margin = new MarginPadding { Top = 5 } - }, - new Box - { - RelativeSizeAxes = Axes.X, - Height = 1.5f, - Margin = new MarginPadding { Top = 10 }, - Colour = colourProvider.Light1, - }, - new FillFlowContainer + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0, 1), + Radius = 3, + Colour = Colour4.Black.Opacity(0.25f), + } + }, + new OsuContextMenuContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Child = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5 }, - Direction = FillDirection.Horizontal, + Direction = FillDirection.Vertical, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Children = new Drawable[] { - userFlag = new UpdateableFlag + new FillFlowContainer { - Size = new Vector2(28, 20), - ShowPlaceholderOnUnknown = false, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + usernameText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular) + }, + supporterTag = new SupporterIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 15, + }, + openUserExternally = new ExternalLinkButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + groupBadgeFlow = new GroupBadgeFlow + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } }, - userCountryText = new OsuSpriteText + titleText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular), - Margin = new MarginPadding { Left = 10 }, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Colour = colourProvider.Light1, - } + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), + Margin = new MarginPadding { Bottom = 5 } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + userFlag = new UpdateableFlag + { + Size = new Vector2(28, 20), + ShowPlaceholderOnUnknown = false, + }, + userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + Margin = new MarginPadding { Left = 5 }, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + } + } + }, } }, - } + }, } + }, + coverToggle = new ToggleCoverButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + CoverExpanded = { BindTarget = coverExpanded } } - } - } - } + }, + }, + }, }, - userStats = new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Y, - Width = 300, - Margin = new MarginPadding { Right = UserProfileOverlay.CONTENT_X_MARGIN }, - Padding = new MarginPadding { Vertical = 15 }, - Spacing = new Vector2(0, 2) - } }; + } - User.BindValueChanged(user => updateUser(user.NewValue)); + protected override void LoadComplete() + { + base.LoadComplete(); + + User.BindValueChanged(user => updateUser(user.NewValue), true); + coverExpanded.BindValueChanged(_ => updateCoverState(), true); + FinishTransforms(true); } private void updateUser(UserProfileData? data) { var user = data?.User; + cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; @@ -192,42 +214,27 @@ namespace osu.Game.Overlays.Profile.Header titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); groupBadgeFlow.User.Value = user; - - userStats.Clear(); - - if (user?.Statistics != null) - { - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsRankedScore, user.Statistics.RankedScore.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsHitAccuracy, user.Statistics.DisplayAccuracy)); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsPlayCount, user.Statistics.PlayCount.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalScore, user.Statistics.TotalScore.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsTotalHits, user.Statistics.TotalHits.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsMaximumCombo, user.Statistics.MaxCombo.ToLocalisableString("#,##0"))); - userStats.Add(new UserStatsLine(UsersStrings.ShowStatsReplaysWatchedByOthers, user.Statistics.ReplaysWatched.ToLocalisableString("#,##0"))); - } } - private partial class UserStatsLine : Container + private void updateCoverState() { - public UserStatsLine(LocalisableString left, LocalisableString right) + const float transition_duration = 500; + + bool expanded = coverToggle.CoverExpanded.Value; + + cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint); + avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint); + avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint); + flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint); + } + + private partial class ProfileCoverBackground : UserCoverBackground + { + protected override double LoadDelay => 0; + + public ProfileCoverBackground() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15), - Text = left, - }, - new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), - Text = right, - }, - }; + Masking = true; } } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index c559a1c102..5d753dea7d 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -3,23 +3,17 @@ using System.Diagnostics; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Overlays.Profile.Header; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; -using osu.Game.Users; namespace osu.Game.Overlays.Profile { public partial class ProfileHeader : TabControlOverlayHeader<LocalisableString> { - private UserCoverBackground coverContainer = null!; - public Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); private CentreHeaderContainer centreHeaderContainer; @@ -29,8 +23,6 @@ namespace osu.Game.Overlays.Profile { ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN; - User.ValueChanged += e => updateDisplay(e.NewValue); - TabControl.AddItem(LayoutStrings.HeaderUsersShow); // todo: pending implementation. @@ -41,25 +33,7 @@ namespace osu.Game.Overlays.Profile Debug.Assert(detailHeaderContainer != null); } - protected override Drawable CreateBackground() => - new Container - { - RelativeSizeAxes = Axes.X, - Height = 150, - Masking = true, - Children = new Drawable[] - { - coverContainer = new ProfileCoverBackground - { - RelativeSizeAxes = Axes.Both, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("222").Opacity(0.8f), Color4Extensions.FromHex("222").Opacity(0.2f)) - }, - } - }; + protected override Drawable CreateBackground() => Empty(); protected override Drawable CreateContent() => new FillFlowContainer { @@ -73,7 +47,7 @@ namespace osu.Game.Overlays.Profile RelativeSizeAxes = Axes.X, User = { BindTarget = User }, }, - new MedalHeaderContainer + new BadgeHeaderContainer { RelativeSizeAxes = Axes.X, User = { BindTarget = User }, @@ -103,8 +77,6 @@ namespace osu.Game.Overlays.Profile User = { BindTarget = User } }; - private void updateDisplay(UserProfileData? user) => coverContainer.User = user?.User; - private partial class ProfileHeaderTitle : OverlayTitle { public ProfileHeaderTitle() @@ -113,10 +85,5 @@ namespace osu.Game.Overlays.Profile IconTexture = "Icons/Hexacons/profile"; } } - - private partial class ProfileCoverBackground : UserCoverBackground - { - protected override double LoadDelay => 0; - } } } diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs index 9d5e5db6e6..97c66fdf02 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -17,6 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays { @@ -96,7 +98,7 @@ namespace osu.Game.Overlays FinishTransforms(true); } - public override LocalisableString TooltipText => "revert to default"; + public override LocalisableString TooltipText => CommonStrings.RevertToDefault.ToLower(); protected override bool OnHover(HoverEvent e) { diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs index 4d027cf8cb..9291dfe923 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs @@ -35,6 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = SkinSettingsStrings.GameplayCursorDuringTouch, + Keywords = new[] { @"touchscreen" }, Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 55be06c765..2f68b3a82f 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -70,6 +70,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = GeneralSettingsStrings.OpenOsuFolder, + Keywords = new[] { @"logs", @"files", @"access", "directory" }, Action = () => storage.PresentExternally(), }); diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index d4fd78f0c8..c5274d6223 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -34,6 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections new SettingsButton { Text = GeneralSettingsStrings.RunSetupWizard, + Keywords = new[] { @"first run", @"initial", @"getting started" }, TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription, Action = () => firstRunSetupOverlay?.Show(), }, diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index bad06732d0..f140c14a0b 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -133,6 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider<float> { LabelText = GraphicsSettingsStrings.HorizontalPosition, + Keywords = new[] { "screen", "scaling" }, Current = scalingPositionX, KeyboardStep = 0.01f, DisplayAsPercentage = true @@ -140,6 +141,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider<float> { LabelText = GraphicsSettingsStrings.VerticalPosition, + Keywords = new[] { "screen", "scaling" }, Current = scalingPositionY, KeyboardStep = 0.01f, DisplayAsPercentage = true @@ -147,6 +149,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider<float> { LabelText = GraphicsSettingsStrings.HorizontalScale, + Keywords = new[] { "screen", "scaling" }, Current = scalingSizeX, KeyboardStep = 0.01f, DisplayAsPercentage = true @@ -154,6 +157,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider<float> { LabelText = GraphicsSettingsStrings.VerticalScale, + Keywords = new[] { "screen", "scaling" }, Current = scalingSizeY, KeyboardStep = 0.01f, DisplayAsPercentage = true diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs index dbd7949206..2b478f6af3 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings; - public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" }); + public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" }); public BindingSettings(KeyBindingPanel keyConfig) { diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs new file mode 100644 index 0000000000..8d1b12d5b2 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Input.Handlers.Touch; +using osu.Framework.Localisation; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class TouchSettings : SettingsSubsection + { + private readonly TouchHandler handler; + + public TouchSettings(TouchHandler handler) + { + this.handler = handler; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = CommonStrings.Enabled, + Current = handler.Enabled + }, + }; + } + + public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" }); + + protected override LocalisableString Header => handler.Description; + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 80bf292057..309e2a1401 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance var directoryInfos = target.GetDirectories(); var fileInfos = target.GetFiles(); - if (directoryInfos.Length > 0 || fileInfos.Length > 0) + if (directoryInfos.Length > 0 || fileInfos.Length > 0 || target.Parent == null) { // Quick test for whether there's already an osu! install at the target path. if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME)) @@ -65,7 +65,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance return; } - target = target.CreateSubdirectory("osu-lazer"); + // Not using CreateSubDirectory as it throws unexpectedly when attempting to create a directory when already at the root of a disk. + // See https://cs.github.com/dotnet/runtime/blob/f1bdd5a6182f43f3928b389b03f7bc26f826c8bc/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs#L88-L94 + target = Directory.CreateDirectory(Path.Combine(target.FullName, @"osu-lazer")); } } catch (Exception e) diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index 8b5e0b75b7..d3303e409c 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -42,6 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = UserInterfaceStrings.ModSelectHotkeyStyle, Current = config.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle), ClassicDefault = ModSelectHotkeyStyle.Classic + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.BackgroundBlur, + Current = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur), + ClassicDefault = false, } }; } diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 5091ddc2d0..a837444758 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings public LocalisableString TooltipText { get; set; } + public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>(); + public BindableBool CanBeShown { get; } = new BindableBool(true); IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown; @@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings get { if (TooltipText != default) - return base.FilterTerms.Append(TooltipText); + yield return TooltipText; - return base.FilterTerms; + foreach (string s in Keywords) + yield return s; + + foreach (LocalisableString s in base.FilterTerms) + yield return s; } } } diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 62933ed556..9ab0fa7ad8 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings Text = game.Name, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), }, - new BuildDisplay(game.Version, DebugUtils.IsDebugBuild) + new BuildDisplay(game.Version) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings private partial class BuildDisplay : OsuAnimatedButton { private readonly string version; - private readonly bool isDebug; [Resolved] private OsuColour colours { get; set; } - public BuildDisplay(string version, bool isDebug) + public BuildDisplay(string version) { this.version = version; - this.isDebug = isDebug; Content.RelativeSizeAxes = Axes.Y; Content.AutoSizeAxes = AutoSizeAxes = Axes.X; @@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings [BackgroundDependencyLoader(true)] private void load(ChangelogOverlay changelog) { - if (!isDebug) - Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); + Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version); Add(new OsuSpriteText { @@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre, Padding = new MarginPadding(5), - Colour = isDebug ? colours.Red : Color4.White, + Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White, }); } } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 86964e7ed4..c0948c1eab 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -135,12 +135,14 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { updateFadeState(); + updateExpandedState(true); return false; } protected override void OnHoverLost(HoverLostEvent e) { updateFadeState(); + updateExpandedState(true); base.OnHoverLost(e); } @@ -168,7 +170,7 @@ namespace osu.Game.Overlays // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - if (Expanded.Value) + if (Expanded.Value || IsHovered) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 5a1fe40fbf..dba4e8feb6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Game.Input.Bindings; +using osu.Game.Localisation; namespace osu.Game.Overlays.Toolbar { @@ -19,8 +20,8 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load() { - TooltipMain = "home"; - TooltipSub = "return to the main menu"; + TooltipMain = ToolbarStrings.HomeHeaderTitle; + TooltipSub = ToolbarStrings.HomeHeaderDescription; SetIcon("Icons/Hexacons/home"); } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 8f9930e910..07f7d52545 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -3,12 +3,13 @@ #nullable disable +using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Localisation; using osu.Game.Rulesets; using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Input.Events; namespace osu.Game.Overlays.Toolbar { @@ -29,7 +30,7 @@ namespace osu.Game.Overlays.Toolbar var rInstance = value.CreateInstance(); ruleset.TooltipMain = rInstance.Description; - ruleset.TooltipSub = $"play some {rInstance.Description}"; + ruleset.TooltipSub = ToolbarStrings.PlaySomeRuleset(rInstance.Description); ruleset.SetIcon(rInstance.CreateIcon()); } diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 4e0e45e0f5..3c878ffd33 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Linq; using osu.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>(); /// <summary> - /// The screen-space point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag. + /// The screen-space main point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag. /// </summary> public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre; + /// <summary> + /// Any points that should be used for snapping purposes in addition to <see cref="ScreenSpaceSelectionPoint"/>. Exposed via <see cref="ScreenSpaceSnapPoints"/>. + /// </summary> + protected virtual Vector2[] ScreenSpaceAdditionalNodes => Array.Empty<Vector2>(); + + /// <summary> + /// The screen-space collection of base points on this <see cref="HitObjectSelectionBlueprint"/> that other objects can be snapped to. + /// The first element of this collection is <see cref="ScreenSpaceSelectionPoint"/> + /// </summary> + public Vector2[] ScreenSpaceSnapPoints => ScreenSpaceAdditionalNodes.Prepend(ScreenSpaceSelectionPoint).ToArray(); + /// <summary> /// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections. /// </summary> diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs new file mode 100644 index 0000000000..59fbfe49b6 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Judgements; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcessor + { + public override string Name => "Accuracy Challenge"; + + public override string Acronym => "AC"; + + public override LocalisableString Description => "Fail if your accuracy drops too low!"; + + public override ModType Type => ModType.DifficultyIncrease; + + public override double ScoreMultiplier => 1.0; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModEasyWithExtraLives), typeof(ModPerfect) }).ToArray(); + + public override bool RequiresConfiguration => false; + + public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); + + [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider<double, PercentSlider>))] + public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble + { + MinValue = 0.60, + MaxValue = 0.99, + Precision = 0.01, + Default = 0.9, + Value = 0.9, + }; + + private ScoreProcessor scoreProcessor = null!; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor; + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (!result.Type.AffectsAccuracy()) + return false; + + return getAccuracyWithImminentResultAdded(result) < MinimumAccuracy.Value; + } + + private double getAccuracyWithImminentResultAdded(JudgementResult result) + { + var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo }; + + // This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value. + // Hopefully we can improve this in the future. + scoreProcessor.PopulateScore(score); + score.Statistics[result.Type]++; + + return scoreProcessor.ComputeAccuracy(score); + } + } + + public partial class PercentSlider : OsuSliderBar<double> + { + public PercentSlider() + { + DisplayAsPercentage = true; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index c4396e440e..e101ac440e 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using Humanizer; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -19,6 +21,7 @@ namespace osu.Game.Rulesets.Mods }; public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray(); private int retries; diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 804f23b6b7..6f0bb7ad3b 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; public override LocalisableString Description => "SS or quit."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray(); protected ModPerfect() { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index be5a7f71e7..39f0888882 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -303,8 +303,7 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged -= onSamplesChanged; // Release the samples for other hitobjects to use. - if (Samples != null) - Samples.Samples = null; + Samples?.ClearSamples(); foreach (var obj in nestedHitObjects) { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ff0d91c0dd..b5f42ec2cc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring /// </summary> protected virtual double ClassicScoreMultiplier => 36; - private readonly Ruleset ruleset; + /// <summary> + /// The ruleset this score processor is valid for. + /// </summary> + public readonly Ruleset Ruleset; + private readonly double accuracyPortion; private readonly double comboPortion; @@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring public ScoreProcessor(Ruleset ruleset) { - this.ruleset = ruleset; + Ruleset = ruleset; accuracyPortion = DefaultAccuracyPortion; comboPortion = DefaultComboPortion; @@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring [Pure] public double ComputeAccuracy(ScoreInfo scoreInfo) { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); + if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) + throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); // We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap. extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); @@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring [Pure] public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); + if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) + throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); extractScoringValues(scoreInfo, out var current, out var maximum); @@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring break; default: - maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; + maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; break; } diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index c50f63c3b2..96b02ee4dc 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -157,6 +157,8 @@ namespace osu.Game.Rulesets.UI set => throw new NotSupportedException(); } + public void AddExtension(string extension) => throw new NotSupportedException(); + public void Dispose() { if (primary.IsNotNull()) primary.Dispose(); diff --git a/osu.Game/Scoring/RankingTier.cs b/osu.Game/Scoring/RankingTier.cs new file mode 100644 index 0000000000..e57c241515 --- /dev/null +++ b/osu.Game/Scoring/RankingTier.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Scoring +{ + public enum RankingTier + { + Iron, + Bronze, + Silver, + Gold, + Platinum, + Rhodium, + Radiant, + Lustrous + } +} diff --git a/osu.Game/Screens/Edit/BackgroundDimMenuItem.cs b/osu.Game/Screens/Edit/BackgroundDimMenuItem.cs index b5a33f06e7..2a1159eb27 100644 --- a/osu.Game/Screens/Edit/BackgroundDimMenuItem.cs +++ b/osu.Game/Screens/Edit/BackgroundDimMenuItem.cs @@ -5,17 +5,18 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit { internal class BackgroundDimMenuItem : MenuItem { - private readonly Bindable<float> backgroudDim; + private readonly Bindable<float> backgroundDim; private readonly Dictionary<float, TernaryStateRadioMenuItem> menuItemLookup = new Dictionary<float, TernaryStateRadioMenuItem>(); - public BackgroundDimMenuItem(Bindable<float> backgroudDim) - : base("Background dim") + public BackgroundDimMenuItem(Bindable<float> backgroundDim) + : base(GameplaySettingsStrings.BackgroundDim) { Items = new[] { @@ -25,8 +26,8 @@ namespace osu.Game.Screens.Edit createMenuItem(0.75f), }; - this.backgroudDim = backgroudDim; - backgroudDim.BindValueChanged(dim => + this.backgroundDim = backgroundDim; + backgroundDim.BindValueChanged(dim => { foreach (var kvp in menuItemLookup) kvp.Value.State.Value = kvp.Key == dim.NewValue ? TernaryState.True : TernaryState.False; @@ -40,6 +41,6 @@ namespace osu.Game.Screens.Edit return item; } - private void updateOpacity(float dim) => backgroudDim.Value = dim; + private void updateOpacity(float dim) => backgroundDim.Value = dim; } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuItem.cs index 0a2c073dcd..368fe40977 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuItem.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuItem.cs @@ -2,21 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Edit.Components.Menus { public class EditorMenuItem : OsuMenuItem { - private const int min_text_length = 40; - - public EditorMenuItem(string text, MenuItemType type = MenuItemType.Standard) - : base(text.PadRight(min_text_length), type) + public EditorMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard) + : base(text, type) { } - public EditorMenuItem(string text, MenuItemType type, Action action) - : base(text.PadRight(min_text_length), type, action) + public EditorMenuItem(LocalisableString text, MenuItemType type, Action action) + : base(text, type, action) { } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs b/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs index 3d51874082..1f6d61d0ad 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorScreenSwitcherControl.cs @@ -56,11 +56,6 @@ namespace osu.Game.Screens.Edit.Components.Menus Bar.Expire(); } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - } - protected override void OnActivated() { base.OnActivated(); diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 87cbcb8aff..72c299f443 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; using osuTK.Input; @@ -47,7 +48,7 @@ namespace osu.Game.Screens.Edit.Components new OsuSpriteText { Origin = Anchor.BottomLeft, - Text = "Playback speed", + Text = EditorStrings.PlaybackSpeed, RelativePositionAxes = Axes.Y, Y = 0.5f, Padding = new MarginPadding { Left = 45 } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 9b45464e81..169e72fe3f 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Components.Timelines.Summary @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Content.CornerRadius = 0; - Text = "Test!"; + Text = EditorStrings.TestBeatmap; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 77892f21e6..e4e67d10d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -439,7 +439,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2[] movementBlueprintOriginalPositions; + private Vector2[][] movementBlueprintsOriginalPositions; private SelectionBlueprint<T>[] movementBlueprints; private bool isDraggingBlueprint; @@ -459,7 +459,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); - movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); + movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray(); return true; } @@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - Debug.Assert(movementBlueprintOriginalPositions != null); + Debug.Assert(movementBlueprintsOriginalPositions != null); Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; if (snapProvider != null) { - // check for positional snap for every object in selection (for things like object-object snapping) - for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++) + for (int i = 0; i < movementBlueprints.Length; i++) { - Vector2 originalPosition = movementBlueprintOriginalPositions[i]; - var testPosition = originalPosition + distanceTravelled; - - var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); - - if (positionalResult.ScreenSpacePosition == testPosition) continue; - - var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint; - - // attempt to move the objects, and abort any time based snapping if we can. - if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints[i], delta))) + if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i])) return true; } } @@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // item in the selection. // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; + Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled; // Retrieve a snapped position. var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); @@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components return ApplySnapResult(movementBlueprints, result); } + /// <summary> + /// Check for positional snap for given blueprint. + /// </summary> + /// <param name="blueprint">The blueprint to check for snapping.</param> + /// <param name="distanceTravelled">Distance travelled since start of dragging action.</param> + /// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param> + /// <returns>Whether an object to snap to was found.</returns> + private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<T> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) + { + var currentPositions = blueprint.ScreenSpaceSnapPoints; + + for (int i = 0; i < originalPositions.Length; i++) + { + Vector2 originalPosition = originalPositions[i]; + var testPosition = originalPosition + distanceTravelled; + + var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); + + if (positionalResult.ScreenSpacePosition == testPosition) continue; + + var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; + + // attempt to move the objects, and abort any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprint, delta))) + return true; + } + + return false; + } + protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) => SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); @@ -533,7 +552,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - movementBlueprintOriginalPositions = null; + movementBlueprintsOriginalPositions = null; movementBlueprints = null; return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 615925ff91..0b83258f8b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -9,7 +9,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -75,17 +77,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { waveformCheckbox = new OsuCheckbox { - LabelText = "Waveform", + LabelText = EditorStrings.TimelineWaveform, Current = { Value = true }, }, ticksCheckbox = new OsuCheckbox { - LabelText = "Ticks", + LabelText = EditorStrings.TimelineTicks, Current = { Value = true }, }, controlPointsCheckbox = new OsuCheckbox { - LabelText = "BPM", + LabelText = BeatmapsetsStrings.ShowStatsBpm, Current = { Value = true }, }, } diff --git a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs index 85466c5056..811da5236e 100644 --- a/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs +++ b/osu.Game/Screens/Edit/CreateNewDifficultyDialog.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit { @@ -20,7 +19,7 @@ namespace osu.Game.Screens.Edit public CreateNewDifficultyDialog(CreateNewDifficulty createNewDifficulty) { - HeaderText = "Would you like to create a blank difficulty?"; + HeaderText = EditorDialogsStrings.NewDifficultyDialogHeader; Icon = FontAwesome.Regular.Clone; @@ -28,17 +27,17 @@ namespace osu.Game.Screens.Edit { new PopupDialogOkButton { - Text = "Yeah, let's start from scratch!", + Text = EditorDialogsStrings.CreateNew, Action = () => createNewDifficulty.Invoke(false) }, new PopupDialogCancelButton { - Text = "No, create an exact copy of this difficulty", + Text = EditorDialogsStrings.CreateCopy, Action = () => createNewDifficulty.Invoke(true) }, new PopupDialogCancelButton { - Text = "I changed my mind, I want to keep editing this difficulty", + Text = EditorDialogsStrings.KeepEditing, Action = () => { } } }; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index be4e2f9628..74ea933255 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -51,7 +51,7 @@ using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.Play; using osu.Game.Users; using osuTK.Input; -using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Screens.Edit { @@ -294,40 +294,40 @@ namespace osu.Game.Screens.Edit RelativeSizeAxes = Axes.Both, Items = new[] { - new MenuItem("File") + new MenuItem(CommonStrings.MenuBarFile) { Items = createFileMenuItems() }, - new MenuItem(CommonStrings.ButtonsEdit) + new MenuItem(CommonStrings.MenuBarEdit) { Items = new[] { - undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), new EditorMenuItemSpacer(), - cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem("Clone", MenuItemType.Standard, Clone), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), } }, - new MenuItem("View") + new MenuItem(CommonStrings.MenuBarView) { Items = new MenuItem[] { new WaveformOpacityMenuItem(config.GetBindable<float>(OsuSetting.EditorWaveformOpacity)), new BackgroundDimMenuItem(editorBackgroundDim), - new ToggleMenuItem("Show hit markers") + new ToggleMenuItem(EditorStrings.ShowHitMarkers) { State = { BindTarget = editorHitMarkers }, } } }, - new MenuItem("Timing") + new MenuItem(EditorStrings.Timing) { Items = new MenuItem[] { - new EditorMenuItem("Set preview point to current time", MenuItemType.Standard, SetPreviewPointToCurrentTime) + new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime) } } } @@ -344,7 +344,6 @@ namespace osu.Game.Screens.Edit bottomBar = new BottomBar(), } }); - changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); @@ -716,7 +715,7 @@ namespace osu.Game.Screens.Edit if (!(refetchedBeatmap is DummyWorkingBeatmap)) { - Logger.Log("Editor providing re-fetched beatmap post edit session"); + Logger.Log(@"Editor providing re-fetched beatmap post edit session"); Beatmap.Value = refetchedBeatmap; } } @@ -952,15 +951,15 @@ namespace osu.Game.Screens.Edit private List<MenuItem> createFileMenuItems() => new List<MenuItem> { - new EditorMenuItem("Save", MenuItemType.Standard, () => Save()), - new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, new EditorMenuItemSpacer(), createDifficultyCreationMenu(), createDifficultySwitchMenu(), new EditorMenuItemSpacer(), - new EditorMenuItem("Delete difficulty", MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, + new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, new EditorMenuItemSpacer(), - new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit) + new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) }; private void exportBeatmap() @@ -1009,7 +1008,7 @@ namespace osu.Game.Screens.Edit foreach (var ruleset in rulesets.AvailableRulesets) rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); - return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; + return new EditorMenuItem(EditorStrings.CreateNewDifficulty) { Items = rulesetItems }; } protected void CreateNewDifficulty(RulesetInfo rulesetInfo) @@ -1045,7 +1044,7 @@ namespace osu.Game.Screens.Edit } } - return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; + return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems }; } protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); diff --git a/osu.Game/Screens/Edit/EditorScreenMode.cs b/osu.Game/Screens/Edit/EditorScreenMode.cs index 81fcf23cdf..f787fee1e0 100644 --- a/osu.Game/Screens/Edit/EditorScreenMode.cs +++ b/osu.Game/Screens/Edit/EditorScreenMode.cs @@ -1,27 +1,26 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit { public enum EditorScreenMode { - [Description("setup")] + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.SetupScreen))] SongSetup, - [Description("compose")] + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.ComposeScreen))] Compose, - [Description("design")] + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.DesignScreen))] Design, - [Description("timing")] + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.TimingScreen))] Timing, - [Description("verify")] + [LocalisableDescription(typeof(EditorStrings), nameof(EditorStrings.VerifyScreen))] Verify, } } diff --git a/osu.Game/Screens/Edit/PromptForSaveDialog.cs b/osu.Game/Screens/Edit/PromptForSaveDialog.cs index 2a2cd019ea..7d78465e6c 100644 --- a/osu.Game/Screens/Edit/PromptForSaveDialog.cs +++ b/osu.Game/Screens/Edit/PromptForSaveDialog.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit { @@ -13,7 +12,7 @@ namespace osu.Game.Screens.Edit { public PromptForSaveDialog(Action exit, Action saveAndExit, Action cancel) { - HeaderText = "Did you want to save your changes?"; + HeaderText = EditorDialogsStrings.SaveDialogHeader; Icon = FontAwesome.Regular.Save; @@ -21,17 +20,17 @@ namespace osu.Game.Screens.Edit { new PopupDialogOkButton { - Text = @"Save my masterpiece!", + Text = EditorDialogsStrings.Save, Action = saveAndExit }, new PopupDialogDangerousButton { - Text = @"Forget all changes", + Text = EditorDialogsStrings.ForgetAllChanges, Action = exit }, new PopupDialogCancelButton { - Text = @"Oops, continue editing", + Text = EditorDialogsStrings.ContinueEditing, Action = cancel }, }; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs index 47a7512adf..033e5361bb 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeaderBackground.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -61,7 +62,7 @@ namespace osu.Game.Screens.Edit.Setup }, new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24)) { - Text = "Drag image here to set beatmap background!", + Text = EditorSetupStrings.DragToSetBackground, Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs index 3d3f67e70e..5b1d7142e4 100644 --- a/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs +++ b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit { @@ -17,7 +18,7 @@ namespace osu.Game.Screens.Edit private readonly Dictionary<float, TernaryStateRadioMenuItem> menuItemLookup = new Dictionary<float, TernaryStateRadioMenuItem>(); public WaveformOpacityMenuItem(Bindable<float> waveformOpacity) - : base("Waveform opacity") + : base(EditorStrings.WaveformOpacity) { Items = new[] { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index b9d8912170..e0ae437d49 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay IsValidMod = IsValidMod }; - protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() { var buttons = base.CreateFooterButtons().ToList(); buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay)); diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 3f21f811c1..0214d33549 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Play private const float duration = 2500; private ISample? failSample; + private SampleChannel? failSampleChannel; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -119,13 +120,13 @@ namespace osu.Game.Screens.Play this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ => { // Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep. - RemoveFilters(false); + removeFilters(false); OnComplete?.Invoke(); }); failHighPassFilter.CutoffTo(300); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); - failSample?.Play(); + failSampleChannel = failSample?.Play(); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); @@ -153,7 +154,16 @@ namespace osu.Game.Screens.Play Background?.FadeColour(OsuColour.Gray(0.3f), 60); } - public void RemoveFilters(bool resetTrackFrequency = true) + /// <summary> + /// Stops any and all persistent effects added by the ongoing fail animation. + /// </summary> + public void Stop() + { + failSampleChannel?.Stop(); + removeFilters(); + } + + private void removeFilters(bool resetTrackFrequency = true) { filtersRemoved = true; diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index c681ef8f96..81146a4ea6 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play /// <summary> /// Action that is invoked when <see cref="GlobalAction.Back"/> is triggered. /// </summary> - protected virtual Action BackAction => () => InternalButtons.Children.LastOrDefault()?.TriggerClick(); + protected virtual Action BackAction => () => InternalButtons.LastOrDefault()?.TriggerClick(); /// <summary> /// Action that is invoked when <see cref="GlobalAction.Select"/> is triggered. @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Play InternalButtons.Add(button); } - public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) + public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e) { switch (e.Action) { diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs new file mode 100644 index 0000000000..bfee6d295e --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -0,0 +1,117 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonSongProgress : SongProgress + { + private readonly SongProgressInfo info; + private readonly ArgonSongProgressGraph graph; + private readonly ArgonSongProgressBar bar; + private readonly Container graphContainer; + + private const float bar_height = 10; + + [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")] + public Bindable<bool> ShowGraph { get; } = new BindableBool(true); + + [Resolved] + private Player? player { get; set; } + + public ArgonSongProgress() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + Masking = true; + CornerRadius = 5; + Children = new Drawable[] + { + info = new SongProgressInfo + { + Origin = Anchor.TopLeft, + Name = "Info", + Anchor = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + ShowProgress = false + }, + bar = new ArgonSongProgressBar(bar_height) + { + Name = "Seek bar", + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + OnSeek = time => player?.Seek(time), + }, + graphContainer = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Masking = true, + CornerRadius = 5, + Child = graph = new ArgonSongProgressGraph + { + Name = "Difficulty graph", + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive + }, + RelativeSizeAxes = Axes.X, + }, + }; + RelativeSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load() + { + info.TextColour = Colour4.White; + info.Font = OsuFont.Torus.With(size: 18, weight: FontWeight.Bold); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); + ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); + } + + protected override void UpdateObjects(IEnumerable<HitObject> objects) + { + graph.Objects = objects; + + info.StartTime = bar.StartTime = FirstHitTime; + info.EndTime = bar.EndTime = LastHitTime; + } + + private void updateGraphVisibility() + { + graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In); + bar.ShowBackground = !ShowGraph.Value; + } + + protected override void Update() + { + base.Update(); + Height = bar.Height + bar_height + info.Height; + graphContainer.Height = bar.Height; + } + + protected override void UpdateProgress(double progress, bool isIntro) + { + bar.TrackTime = GameplayClock.CurrentTime; + + if (isIntro) + bar.CurrentTime = 0; + else + bar.CurrentTime = FrameStableClock.CurrentTime; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs new file mode 100644 index 0000000000..6db1072fbb --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -0,0 +1,266 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonSongProgressBar : SliderBar<double> + { + public Action<double>? OnSeek { get; set; } + + // Parent will handle restricting the area of valid input. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + private readonly float barHeight; + + private readonly RoundedBar playfieldBar; + private readonly RoundedBar catchupBar; + + private readonly Box background; + + private readonly BindableBool showBackground = new BindableBool(); + + private readonly ColourInfo mainColour; + private readonly ColourInfo mainColourDarkened; + private ColourInfo catchUpColour; + private ColourInfo catchUpColourDarkened; + + public bool ShowBackground + { + get => showBackground.Value; + set => showBackground.Value = value; + } + + public double StartTime + { + private get => CurrentNumber.MinValue; + set => CurrentNumber.MinValue = value; + } + + public double EndTime + { + private get => CurrentNumber.MaxValue; + set => CurrentNumber.MaxValue = value; + } + + public double CurrentTime + { + private get => CurrentNumber.Value; + set => CurrentNumber.Value = value; + } + + public double TrackTime + { + private get => currentTrackTime.Value; + set => currentTrackTime.Value = value; + } + + private double length => EndTime - StartTime; + + private readonly BindableNumber<double> currentTrackTime; + + public bool Interactive { get; set; } + + public ArgonSongProgressBar(float barHeight) + { + currentTrackTime = new BindableDouble(); + setupAlternateValue(); + + StartTime = 0; + EndTime = 1; + + RelativeSizeAxes = Axes.X; + Height = this.barHeight = barHeight; + + CornerRadius = 5; + Masking = true; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Colour4.White.Darken(1 + 1 / 4f) + }, + catchupBar = new RoundedBar + { + Name = "Audio bar", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + CornerRadius = 5, + AlwaysPresent = true, + RelativeSizeAxes = Axes.Both + }, + playfieldBar = new RoundedBar + { + Name = "Playfield bar", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + CornerRadius = 5, + AccentColour = mainColour = Color4.White, + RelativeSizeAxes = Axes.Both + }, + }; + + mainColourDarkened = Colour4.White.Darken(1 / 3f); + } + + private void setupAlternateValue() + { + CurrentNumber.MaxValueChanged += v => currentTrackTime.MaxValue = v; + CurrentNumber.MinValueChanged += v => currentTrackTime.MinValue = v; + CurrentNumber.PrecisionChanged += v => currentTrackTime.Precision = v; + } + + private float normalizedReference + { + get + { + if (EndTime - StartTime == 0) + return 1; + + return (float)((TrackTime - StartTime) / length); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + catchUpColour = colours.BlueLight; + catchUpColourDarkened = colours.BlueDark; + + showBackground.BindValueChanged(_ => updateBackground(), true); + } + + private void updateBackground() + { + background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In); + playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In); + } + + protected override bool OnHover(HoverEvent e) + { + if (Interactive) + this.ResizeHeightTo(barHeight * 3.5f, 200, Easing.Out); + + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.ResizeHeightTo(barHeight, 800, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override void UpdateValue(float value) + { + // Handled in Update + } + + protected override void Update() + { + base.Update(); + + playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, NormalizedValue, Math.Clamp(Time.Elapsed / 40, 0, 1)); + catchupBar.Length = (float)Interpolation.Lerp(catchupBar.Length, normalizedReference, Math.Clamp(Time.Elapsed / 40, 0, 1)); + + if (TrackTime < CurrentTime) + ChangeChildDepth(catchupBar, -1); + else + ChangeChildDepth(catchupBar, 0); + + float timeDelta = (float)(Math.Abs(CurrentTime - TrackTime)); + + const float colour_transition_threshold = 20000; + + catchupBar.AccentColour = Interpolation.ValueAt( + Math.Min(timeDelta, colour_transition_threshold), + ShowBackground ? mainColour : mainColourDarkened, + ShowBackground ? catchUpColour : catchUpColourDarkened, + 0, colour_transition_threshold, + Easing.OutQuint); + + catchupBar.Alpha = Math.Max(1, catchupBar.Length); + } + + private ScheduledDelegate? scheduledSeek; + + protected override void OnUserChange(double value) + { + scheduledSeek?.Cancel(); + scheduledSeek = Schedule(() => + { + if (Interactive) + OnSeek?.Invoke(value); + }); + } + + private partial class RoundedBar : Container + { + private readonly Box fill; + private readonly Container mask; + private float length; + + public RoundedBar() + { + Masking = true; + Children = new[] + { + mask = new Container + { + Masking = true, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(1), + Child = fill = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + } + } + }; + } + + public float Length + { + get => length; + set + { + length = value; + mask.Width = value * DrawWidth; + fill.Width = value * DrawWidth; + } + } + + public new float CornerRadius + { + get => base.CornerRadius; + set + { + base.CornerRadius = value; + mask.CornerRadius = value; + } + } + + public ColourInfo AccentColour + { + get => fill.Colour; + set => fill.Colour = value; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs new file mode 100644 index 0000000000..0899476ed4 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonSongProgressGraph : SegmentedGraph<int> + { + private IEnumerable<HitObject>? objects; + + public IEnumerable<HitObject> Objects + { + set + { + objects = value; + + const int granularity = 200; + int[] values = new int[granularity]; + + if (!objects.Any()) + return; + + double firstHit = objects.First().StartTime; + double lastHit = objects.Max(o => o.GetEndTime()); + + if (lastHit == 0) + lastHit = objects.Last().StartTime; + + double interval = (lastHit - firstHit + 1) / granularity; + + foreach (var h in objects) + { + double endTime = h.GetEndTime(); + + Debug.Assert(endTime >= h.StartTime); + + int startRange = (int)((h.StartTime - firstHit) / interval); + int endRange = (int)((endTime - firstHit) / interval); + for (int i = startRange; i <= endRange; i++) + values[i]++; + } + + Values = values; + } + } + + public ArgonSongProgressGraph() + : base(5) + { + var colours = new List<Colour4>(); + + for (int i = 0; i < 5; i++) + colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f)); + + TierColours = colours; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index bf1f508d7b..ba0c47dc8b 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -15,14 +15,12 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond [Resolved] private IGameplayClock gameplayClock { get; set; } = null!; - [Resolved(canBeNull: true)] - private DrawableRuleset? drawableRuleset { get; set; } + [Resolved] + private IFrameStableClock? frameStableClock { get; set; } public int Value { get; private set; } - // Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset` - // as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency. - private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock; + private IGameplayClock clock => frameStableClock ?? gameplayClock; public ClicksPerSecondCalculator() { diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 76027f9e5d..62d66efb33 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -78,30 +78,39 @@ namespace osu.Game.Screens.Play.HUD public DefaultHealthDisplay() { - Size = new Vector2(1, 5); - RelativeSizeAxes = Axes.X; - Margin = new MarginPadding { Top = 20 }; + const float padding = 20; + const float bar_height = 5; - InternalChildren = new Drawable[] + Size = new Vector2(1, bar_height + padding * 2); + RelativeSizeAxes = Axes.X; + + InternalChild = new Container { - new Box + Padding = new MarginPadding { Vertical = padding }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - fill = new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0, 1), - Masking = true, - Children = new[] + new Box { - new Box + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + fill = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0, 1), + Masking = true, + Children = new[] { - RelativeSizeAxes = Axes.Both, + new Box + { + RelativeSizeAxes = Axes.Both, + } } - } - }, + }, + } }; } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 53866312a0..91a34fe374 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -23,27 +22,16 @@ namespace osu.Game.Screens.Play.HUD private const float transition_duration = 200; - private readonly SongProgressBar bar; - private readonly SongProgressGraph graph; + private readonly DefaultSongProgressBar bar; + private readonly DefaultSongProgressGraph graph; private readonly SongProgressInfo info; - /// <summary> - /// Whether seeking is allowed and the progress bar should be shown. - /// </summary> - public readonly Bindable<bool> AllowSeeking = new Bindable<bool>(); - [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")] public Bindable<bool> ShowGraph { get; } = new BindableBool(true); - public override bool HandleNonPositionalInput => AllowSeeking.Value; - public override bool HandlePositionalInput => AllowSeeking.Value; - [Resolved] private Player? player { get; set; } - [Resolved] - private DrawableRuleset? drawableRuleset { get; set; } - public DefaultSongProgress() { RelativeSizeAxes = Axes.X; @@ -58,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, }, - graph = new SongProgressGraph + graph = new DefaultSongProgressGraph { RelativeSizeAxes = Axes.X, Origin = Anchor.BottomLeft, @@ -66,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD Height = graph_height, Margin = new MarginPadding { Bottom = bottom_bar_height }, }, - bar = new SongProgressBar(bottom_bar_height, graph_height, handle_size) + bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -75,34 +63,18 @@ namespace osu.Game.Screens.Play.HUD }; } - [BackgroundDependencyLoader(true)] + [BackgroundDependencyLoader] private void load(OsuColour colours) { - base.LoadComplete(); - - if (drawableRuleset != null) - { - if (player?.Configuration.AllowUserInteraction == true) - ((IBindable<bool>)AllowSeeking).BindTo(drawableRuleset.HasReplayLoaded); - } - graph.FillColour = bar.FillColour = colours.BlueLighter; } protected override void LoadComplete() { - AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true); + Interactive.BindValueChanged(_ => updateBarVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); - } - protected override void PopIn() - { - this.FadeIn(500, Easing.OutQuint); - } - - protected override void PopOut() - { - this.FadeOut(100); + base.LoadComplete(); } protected override void UpdateObjects(IEnumerable<HitObject> objects) @@ -133,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD private void updateBarVisibility() { - bar.ShowHandle = AllowSeeking.Value; + bar.Interactive = Interactive.Value; updateInfoMargin(); } @@ -150,7 +122,7 @@ namespace osu.Game.Screens.Play.HUD private void updateInfoMargin() { - float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); + float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In); } } diff --git a/osu.Game/Screens/Play/HUD/SongProgressBar.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs similarity index 87% rename from osu.Game/Screens/Play/HUD/SongProgressBar.cs rename to osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs index 28059d4911..0e16067dcc 100644 --- a/osu.Game/Screens/Play/HUD/SongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgressBar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osuTK; using osuTK.Graphics; @@ -15,17 +13,17 @@ using osu.Framework.Threading; namespace osu.Game.Screens.Play.HUD { - public partial class SongProgressBar : SliderBar<double> + public partial class DefaultSongProgressBar : SliderBar<double> { - public Action<double> OnSeek; + /// <summary> + /// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation. + /// </summary> + public Action<double>? OnSeek { get; set; } - private readonly Box fill; - private readonly Container handleBase; - private readonly Container handleContainer; - - private bool showHandle; - - public bool ShowHandle + /// <summary> + /// Whether the progress bar should allow interaction, ie. to perform seek operations. + /// </summary> + public bool Interactive { get => showHandle; set @@ -59,7 +57,13 @@ namespace osu.Game.Screens.Play.HUD set => CurrentNumber.Value = value; } - public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize) + private readonly Box fill; + private readonly Container handleBase; + private readonly Container handleContainer; + + private bool showHandle; + + public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize) { CurrentNumber.MinValue = 0; CurrentNumber.MaxValue = 1; @@ -142,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD handleBase.X = newX; } - private ScheduledDelegate scheduledSeek; + private ScheduledDelegate? scheduledSeek; protected override void OnUserChange(double value) { diff --git a/osu.Game/Screens/Play/HUD/SongProgressGraph.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs similarity index 95% rename from osu.Game/Screens/Play/HUD/SongProgressGraph.cs rename to osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs index f69a1eccd6..bee5978817 100644 --- a/osu.Game/Screens/Play/HUD/SongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Play.HUD { - public partial class SongProgressGraph : SquareGraph + public partial class DefaultSongProgressGraph : SquareGraph { private IEnumerable<HitObject> objects; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs new file mode 100644 index 0000000000..7675d0cc4f --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + public partial class JudgementCounter : VisibilityContainer + { + public BindableBool ShowName = new BindableBool(); + public Bindable<FillDirection> Direction = new Bindable<FillDirection>(); + + public readonly JudgementTally.JudgementCount Result; + + public JudgementCounter(JudgementTally.JudgementCount result) => Result = result; + + public OsuSpriteText ResultName = null!; + private FillFlowContainer flowContainer = null!; + private JudgementRollingCounter counter = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IBindable<RulesetInfo> ruleset) + { + AutoSizeAxes = Axes.Both; + + InternalChild = flowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + counter = new JudgementRollingCounter + { + Current = Result.ResultCount + }, + ResultName = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.Numeric.With(size: 8), + Text = ruleset.Value.CreateInstance().GetDisplayNameForHitResult(Result.Type) + } + } + }; + + var result = Result.Type; + + Colour = result.IsBasic() ? colours.ForHitResult(Result.Type) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; + } + + protected override void LoadComplete() + { + ShowName.BindValueChanged(value => + ResultName.FadeTo(value.NewValue ? 1 : 0, JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint), true); + + Direction.BindValueChanged(direction => + { + flowContainer.Direction = direction.NewValue; + changeAnchor(direction.NewValue == FillDirection.Vertical ? Anchor.TopLeft : Anchor.BottomLeft); + + void changeAnchor(Anchor anchor) => counter.Anchor = ResultName.Anchor = counter.Origin = ResultName.Origin = anchor; + }, true); + + base.LoadComplete(); + } + + protected override void PopIn() => this.FadeIn(JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(100); + + private sealed partial class JudgementRollingCounter : RollingCounter<int> + { + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true, size: 16)); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs new file mode 100644 index 0000000000..82f24c736e --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -0,0 +1,138 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + public partial class JudgementCounterDisplay : CompositeDrawable, ISkinnableDrawable + { + public const int TRANSFORM_DURATION = 250; + + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Display mode")] + public Bindable<DisplayMode> Mode { get; set; } = new Bindable<DisplayMode>(); + + [SettingSource("Counter direction")] + public Bindable<Direction> FlowDirection { get; set; } = new Bindable<Direction>(); + + [SettingSource("Show judgement names")] + public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true); + + [SettingSource("Show max judgement")] + public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true); + + [Resolved] + private JudgementTally tally { get; set; } = null!; + + protected FillFlowContainer<JudgementCounter> CounterFlow = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + InternalChild = CounterFlow = new FillFlowContainer<JudgementCounter> + { + Direction = getFillDirection(FlowDirection.Value), + Spacing = new Vector2(10), + AutoSizeAxes = Axes.Both + }; + + foreach (var result in tally.Results) + CounterFlow.Add(createCounter(result)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + FlowDirection.BindValueChanged(direction => + { + var convertedDirection = getFillDirection(direction.NewValue); + + CounterFlow.Direction = convertedDirection; + + foreach (var counter in CounterFlow.Children) + counter.Direction.Value = convertedDirection; + }, true); + + Mode.BindValueChanged(_ => updateDisplay()); + ShowMaxJudgement.BindValueChanged(_ => updateDisplay(), true); + } + + private void updateDisplay() + { + for (int i = 0; i < CounterFlow.Children.Count; i++) + { + JudgementCounter counter = CounterFlow.Children[i]; + + if (shouldShow(i, counter)) + counter.Show(); + else + counter.Hide(); + } + + bool shouldShow(int index, JudgementCounter counter) + { + if (index == 0 && !ShowMaxJudgement.Value) + return false; + + if (counter.Result.Type.IsBasic()) + return true; + + switch (Mode.Value) + { + case DisplayMode.Simple: + return false; + + case DisplayMode.Normal: + return !counter.Result.Type.IsBonus(); + + case DisplayMode.All: + return true; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private FillDirection getFillDirection(Direction flow) + { + switch (flow) + { + case Direction.Horizontal: + return FillDirection.Horizontal; + + case Direction.Vertical: + return FillDirection.Vertical; + + default: + throw new ArgumentOutOfRangeException(nameof(flow), flow, @"Unsupported direction"); + } + } + + private JudgementCounter createCounter(JudgementTally.JudgementCount info) => + new JudgementCounter(info) + { + State = { Value = Visibility.Hidden }, + ShowName = { BindTarget = ShowJudgementNames } + }; + + public enum DisplayMode + { + Simple, + Normal, + All + } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs new file mode 100644 index 0000000000..e9e3fde92a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + /// <summary> + /// Keeps track of judgements for a current play session, exposing bindable counts which can + /// be used for display purposes. + /// </summary> + public partial class JudgementTally : CompositeDrawable + { + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } = null!; + + public List<JudgementCount> Results = new List<JudgementCount>(); + + [BackgroundDependencyLoader] + private void load(IBindable<RulesetInfo> ruleset) + { + foreach (var result in ruleset.Value.CreateInstance().GetHitResults()) + { + Results.Add(new JudgementCount + { + Type = result.result, + ResultCount = new BindableInt() + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); + scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); + } + + private void updateCount(JudgementResult judgement, bool revert) + { + foreach (JudgementCount result in Results.Where(result => result.Type == judgement.Type)) + result.ResultCount.Value = revert ? result.ResultCount.Value - 1 : result.ResultCount.Value + 1; + } + + public struct JudgementCount + { + public HitResult Type { get; set; } + + public BindableInt ResultCount { get; set; } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index a885336a3a..45b2c1b13c 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD { //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), - PlaybackSettings = new PlaybackSettings(), + PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } }, VisualSettings = new VisualSettings { Expanded = { Value = false } } } }; diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 4504745eb9..4647c0352b 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Rulesets.Objects; @@ -16,19 +18,33 @@ namespace osu.Game.Screens.Play.HUD { // Some implementations of this element allow seeking during gameplay playback. // Set a sane default of never handling input to override the behaviour provided by OverlayContainer. - public override bool HandleNonPositionalInput => false; - public override bool HandlePositionalInput => false; + public override bool HandleNonPositionalInput => Interactive.Value; + public override bool HandlePositionalInput => Interactive.Value; + protected override bool BlockScrollInput => false; + /// <summary> + /// Whether interaction should be allowed (ie. seeking). If <c>false</c>, interaction controls will not be displayed. + /// </summary> + /// <remarks> + /// By default, this will be automatically decided based on the gameplay state. + /// </remarks> + public readonly Bindable<bool> Interactive = new Bindable<bool>(); + public bool UsesFixedAnchor { get; set; } [Resolved] protected IGameplayClock GameplayClock { get; private set; } = null!; - [Resolved(canBeNull: true)] - private DrawableRuleset? drawableRuleset { get; set; } + [Resolved] + private IFrameStableClock? frameStableClock { get; set; } + + /// <summary> + /// The reference clock is used to accurately tell the current playfield's time (including catch-up lag). + /// However, if none is available (i.e. used in tests), we fall back to the gameplay clock. + /// </summary> + protected IClock FrameStableClock => frameStableClock ?? GameplayClock; - private IClock? referenceClock; private IEnumerable<HitObject>? objects; public IEnumerable<HitObject> Objects @@ -58,15 +74,21 @@ namespace osu.Game.Screens.Play.HUD protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { } [BackgroundDependencyLoader] - private void load() + private void load(DrawableRuleset? drawableRuleset, Player? player) { if (drawableRuleset != null) { + if (player?.Configuration.AllowUserInteraction == true) + ((IBindable<bool>)Interactive).BindTo(drawableRuleset.HasReplayLoaded); + Objects = drawableRuleset.Objects; - referenceClock = drawableRuleset.FrameStableClock; } } + protected override void PopIn() => this.FadeIn(500, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(100); + protected override void Update() { base.Update(); @@ -74,9 +96,7 @@ namespace osu.Game.Screens.Play.HUD if (objects == null) return; - // The reference clock is used to accurately tell the playfield's time. This is obtained from the drawable ruleset. - // However, if no drawable ruleset is available (i.e. used in tests), we fall back to the gameplay clock. - double currentTime = referenceClock?.CurrentTime ?? GameplayClock.CurrentTime; + double currentTime = FrameStableClock.CurrentTime; bool isInIntro = currentTime < FirstHitTime; diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs index fb5f5cc916..7f9f353ded 100644 --- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs +++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs @@ -10,6 +10,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using System; +using osu.Framework.Graphics.Sprites; namespace osu.Game.Screens.Play.HUD { @@ -27,13 +28,33 @@ namespace osu.Game.Screens.Play.HUD private double songLength => endTime - startTime; - private const int margin = 10; + public FontUsage Font + { + set + { + timeCurrent.Font = value; + timeLeft.Font = value; + progress.Font = value; + } + } + + public Colour4 TextColour + { + set + { + timeCurrent.Colour = value; + timeLeft.Colour = value; + progress.Colour = value; + } + } public double StartTime { set => startTime = value; } + public bool ShowProgress { get; init; } = true; + public double EndTime { set => endTime = value; @@ -76,6 +97,7 @@ namespace osu.Game.Screens.Play.HUD Origin = Anchor.Centre, Anchor = Anchor.Centre, AutoSizeAxes = Axes.Both, + Alpha = ShowProgress ? 1 : 0, Child = new UprightAspectMaintainingContainer { Origin = Anchor.Centre, @@ -99,15 +121,15 @@ namespace osu.Game.Screens.Play.HUD AutoSizeAxes = Axes.Both, Child = new UprightAspectMaintainingContainer { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Scaling = ScaleMode.Vertical, ScalingFactor = 0.5f, Child = timeLeft = new SizePreservingSpriteText { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, Colour = colours.BlueLighter, Font = OsuFont.Numeric, } @@ -128,7 +150,7 @@ namespace osu.Game.Screens.Play.HUD if (currentPercent != previousPercent) { - progress.Text = currentPercent.ToString() + @"%"; + progress.Text = currentPercent + @"%"; previousPercent = currentPercent; } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 076d43c077..4d1f0b96b6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; +using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; using osu.Game.Localisation; @@ -59,6 +60,9 @@ namespace osu.Game.Screens.Play [Cached] private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; + [Cached] + private readonly JudgementTally tally; + public Bindable<bool> ShowHealthBar = new Bindable<bool>(true); private readonly DrawableRuleset drawableRuleset; @@ -104,6 +108,8 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { CreateFailingLayer(), + //Needs to be initialized before skinnable drawables. + tally = new JudgementTally(), mainComponents = new MainComponentsContainer { AlwaysPresent = true, diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 28044653e6..d9c60519ad 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -8,8 +8,10 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Input.Bindings; using osu.Game.Skinning; using osuTK.Graphics; @@ -26,7 +28,7 @@ namespace osu.Game.Screens.Play private SkinnableSound pauseLoop; - protected override Action BackAction => () => InternalButtons.Children.First().TriggerClick(); + protected override Action BackAction => () => InternalButtons.First().TriggerClick(); [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -56,5 +58,17 @@ namespace osu.Game.Screens.Play pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } + + public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) + { + switch (e.Action) + { + case GlobalAction.PauseGameplay: + InternalButtons.First().TriggerClick(); + return true; + } + + return base.OnPressed(e); + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 05133fba35..0d208e6d9b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -309,6 +309,8 @@ namespace osu.Game.Screens.Play }); } + dependencies.CacheAs(DrawableRuleset.FrameStableClock); + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. @@ -1070,7 +1072,7 @@ namespace osu.Game.Screens.Play public override bool OnExiting(ScreenExitEvent e) { screenSuspension?.RemoveAndDisposeImmediately(); - failAnimationLayer?.RemoveFilters(); + failAnimationLayer?.Stop(); if (LoadedBeatmapSuccessfully) { diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index c930513c70..838106e198 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Input.Events; using osu.Game.Overlays; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 19eced08d9..774ecc2c9c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -596,6 +596,9 @@ namespace osu.Game.Screens.Select public void FlushPendingFilterOperations() { + if (!IsLoaded) + return; + if (PendingFilter?.Completed == false) { applyActiveCriteria(false); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index a360ddabc7..c52b81f915 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -61,50 +61,75 @@ namespace osu.Game.Screens.Select.Carousel if (!(other is CarouselBeatmapSet otherSet)) return base.CompareTo(criteria, other); + int comparison = 0; + switch (criteria.Sort) { default: case SortMode.Artist: - return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); + comparison = string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); + break; case SortMode.Title: - return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); + comparison = string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); + break; case SortMode.Author: - return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); + comparison = string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); + break; case SortMode.Source: - return string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase); + comparison = string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase); + break; case SortMode.DateAdded: - return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); + comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); + break; case SortMode.DateRanked: // Beatmaps which have no ranked date should already be filtered away in this mode. if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null) - return 0; + break; - return otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value); + comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value); + break; case SortMode.LastPlayed: - return -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds()); + comparison = -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds()); + break; case SortMode.BPM: - return compareUsingAggregateMax(otherSet, b => b.BPM); + comparison = compareUsingAggregateMax(otherSet, b => b.BPM); + break; case SortMode.Length: - return compareUsingAggregateMax(otherSet, b => b.Length); + comparison = compareUsingAggregateMax(otherSet, b => b.Length); + break; case SortMode.Difficulty: - return compareUsingAggregateMax(otherSet, b => b.StarRating); + comparison = compareUsingAggregateMax(otherSet, b => b.StarRating); + break; case SortMode.DateSubmitted: // Beatmaps which have no submitted date should already be filtered away in this mode. if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) - return 0; + break; - return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value); + comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value); + break; } + + if (comparison != 0) return comparison; + + // If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion. + // The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc). + comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); + + if (comparison != 0) return comparison; + + // If DateAdded fails to break the tie, fallback to our internal GUID for stability. + // This basically means it's a stable random sort. + return otherSet.BeatmapSet.ID.CompareTo(BeatmapSet.ID); } /// <summary> diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 5d0588e67b..a383298faa 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -30,14 +30,16 @@ namespace osu.Game.Screens.Select.Details { public partial class AdvancedStats : Container { + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + [Resolved] private IBindable<IReadOnlyList<Mod>> mods { get; set; } [Resolved] - private IBindable<RulesetInfo> ruleset { get; set; } + private OsuGameBase game { get; set; } - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } + private IBindable<RulesetInfo> gameRuleset; protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; @@ -84,7 +86,13 @@ namespace osu.Game.Screens.Select.Details { base.LoadComplete(); - ruleset.BindValueChanged(_ => updateStatistics()); + // the cached ruleset bindable might be a decoupled bindable provided by SongSelect, + // which we can't rely on in combination with the game-wide selected mods list, + // since mods could be updated to the new ruleset instances while the decoupled bindable is held behind, + // therefore resulting in performing difficulty calculation with invalid states. + gameRuleset = game.Ruleset.GetBoundCopy(); + gameRuleset.BindValueChanged(_ => updateStatistics()); + mods.BindValueChanged(modsChanged, true); } @@ -142,7 +150,14 @@ namespace osu.Game.Screens.Select.Details private CancellationTokenSource starDifficultyCancellationSource; - private void updateStarDifficulty() + /// <summary> + /// Updates the displayed star difficulty statistics with the values provided by the currently-selected beatmap, ruleset, and selected mods. + /// </summary> + /// <remarks> + /// This is scheduled to avoid scenarios wherein a ruleset changes first before selected mods do, + /// potentially resulting in failure during difficulty calculation due to incomplete bindable state updates. + /// </remarks> + private void updateStarDifficulty() => Scheduler.AddOnce(() => { starDifficultyCancellationSource?.Cancel(); @@ -151,8 +166,8 @@ namespace osu.Game.Screens.Select.Details starDifficultyCancellationSource = new CancellationTokenSource(); - var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token); - var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, null, starDifficultyCancellationSource.Token); + var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, mods.Value, starDifficultyCancellationSource.Token); Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() => { @@ -164,7 +179,7 @@ namespace osu.Game.Screens.Select.Details starDifficulty.Value = ((float)normalDifficulty.Value.Stars, (float)moddedDifficulty.Value.Stars); }), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current); - } + }); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index c419501add..5c720c8491 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -104,6 +104,9 @@ namespace osu.Game.Screens.Select.Leaderboards protected override APIRequest? FetchScores(CancellationToken cancellationToken) { + scoreRetrievalRequest?.Cancel(); + scoreRetrievalRequest = null; + var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -152,8 +155,6 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - scoreRetrievalRequest?.Cancel(); - var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); newRequest.Success += response => Schedule(() => { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 38f4b8ef40..61728dd9fd 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1,21 +1,31 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -23,23 +33,12 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; using osu.Game.Screens.Select.Options; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; using osuTK.Input; -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; -using osu.Game.Collections; -using osu.Game.Graphics.UserInterface; -using System.Diagnostics; -using JetBrains.Annotations; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Screens.Play; -using osu.Game.Skinning; namespace osu.Game.Screens.Select { @@ -50,7 +49,7 @@ namespace osu.Game.Screens.Select protected const float BACKGROUND_BLUR = 20; private const float left_area_padding = 20; - public FilterControl FilterControl { get; private set; } + public FilterControl FilterControl { get; private set; } = null!; /// <summary> /// Whether this song select instance should take control of the global track, @@ -65,76 +64,83 @@ namespace osu.Game.Screens.Select /// <summary> /// Can be null if <see cref="ShowFooter"/> is false. /// </summary> - protected BeatmapOptionsOverlay BeatmapOptions { get; private set; } + protected BeatmapOptionsOverlay BeatmapOptions { get; private set; } = null!; /// <summary> /// Can be null if <see cref="ShowFooter"/> is false. /// </summary> - protected Footer Footer { get; private set; } + protected Footer? Footer { get; private set; } /// <summary> /// Contains any panel which is triggered by a footer button. /// Helps keep them located beneath the footer itself. /// </summary> - protected Container FooterPanels { get; private set; } + protected Container FooterPanels { get; private set; } = null!; /// <summary> /// Whether entering editor mode should be allowed. /// </summary> public virtual bool AllowEditing => true; - public bool BeatmapSetsLoaded => IsLoaded && Carousel?.BeatmapSetsLoaded == true; + public bool BeatmapSetsLoaded => IsLoaded && Carousel.BeatmapSetsLoaded; public virtual Func<BeatmapInfo, MenuItem>[] CustomMenuItems => new Func<BeatmapInfo, MenuItem>[] { b => new OsuMenuItem(@"Select", MenuItemType.Highlighted, () => FinaliseSelection(b)) }; [Resolved] - private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } + private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!; - protected BeatmapCarousel Carousel { get; private set; } + protected BeatmapCarousel Carousel { get; private set; } = null!; - private ParallaxContainer wedgeBackground; + private ParallaxContainer wedgeBackground = null!; - protected Container LeftArea { get; private set; } + protected Container LeftArea { get; private set; } = null!; - private BeatmapInfoWedge beatmapInfoWedge; - - [Resolved(canBeNull: true)] - private IDialogOverlay dialogOverlay { get; set; } + private BeatmapInfoWedge beatmapInfoWedge = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } + private IDialogOverlay? dialogOverlay { get; set; } - protected ModSelectOverlay ModSelect { get; private set; } + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; - protected Sample SampleConfirm { get; private set; } + protected ModSelectOverlay ModSelect { get; private set; } = null!; - private Sample sampleChangeDifficulty; - private Sample sampleChangeBeatmap; + protected Sample? SampleConfirm { get; private set; } - private Container carouselContainer; + private Sample sampleChangeDifficulty = null!; + private Sample sampleChangeBeatmap = null!; - protected BeatmapDetailArea BeatmapDetails { get; private set; } + private Container carouselContainer = null!; - private FooterButtonOptions beatmapOptionsButton; + protected BeatmapDetailArea BeatmapDetails { get; private set; } = null!; + + private FooterButtonOptions beatmapOptionsButton = null!; private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>(); private double audioFeedbackLastPlaybackTime; - [CanBeNull] - private IDisposable modSelectOverlayRegistration; + private IDisposable? modSelectOverlayRegistration; [Resolved] - private MusicController music { get; set; } + private MusicController music { get; set; } = null!; - [Resolved(CanBeNull = true)] - internal IOverlayManager OverlayManager { get; private set; } + [Resolved] + internal IOverlayManager? OverlayManager { get; private set; } + + private Bindable<bool> configBackgroundBlur { get; set; } = new BindableBool(); [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender) + private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config) { - // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). - transferRulesetValue(); + configBackgroundBlur = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur); + configBackgroundBlur.BindValueChanged(e => + { + if (!this.IsCurrentScreen()) + return; + + ApplyToBackground(b => b.BlurAmount.Value = e.NewValue ? BACKGROUND_BLUR : 0); + }); LoadComponentAsync(Carousel = new BeatmapCarousel { @@ -149,6 +155,9 @@ namespace osu.Game.Screens.Select GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); + // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). + transferRulesetValue(); + AddRangeInternal(new Drawable[] { new ResetScrollContainer(() => Carousel.ScrollToSelected()) @@ -276,7 +285,7 @@ namespace osu.Game.Screens.Select BeatmapOptions = new BeatmapOptionsOverlay(), } }, - Footer = new Footer(), + Footer = new Footer() }); } @@ -321,7 +330,7 @@ namespace osu.Game.Screens.Select /// Creates the buttons to be displayed in the footer. /// </summary> /// <returns>A set of <see cref="FooterButton"/> and an optional <see cref="OverlayContainer"/> which the button opens when pressed.</returns> - protected virtual IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() => new (FooterButton, OverlayContainer)[] + protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateFooterButtons() => new (FooterButton, OverlayContainer?)[] { (new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom @@ -342,7 +351,7 @@ namespace osu.Game.Screens.Select Carousel.Filter(criteria, shouldDebounce); } - private DependencyContainer dependencies; + private DependencyContainer dependencies = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -360,7 +369,7 @@ namespace osu.Game.Screens.Select /// </summary> protected abstract BeatmapDetailArea CreateBeatmapDetailArea(); - public void Edit(BeatmapInfo beatmapInfo = null) + public void Edit(BeatmapInfo? beatmapInfo = null) { if (!AllowEditing) throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); @@ -375,7 +384,7 @@ namespace osu.Game.Screens.Select /// <param name="beatmapInfo">An optional beatmap to override the current carousel selection.</param> /// <param name="ruleset">An optional ruleset to override the current carousel selection.</param> /// <param name="customStartAction">An optional custom action to perform instead of <see cref="OnStart"/>.</param> - public void FinaliseSelection(BeatmapInfo beatmapInfo = null, RulesetInfo ruleset = null, Action customStartAction = null) + public void FinaliseSelection(BeatmapInfo? beatmapInfo = null, RulesetInfo? ruleset = null, Action? customStartAction = null) { // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. if (!Carousel.BeatmapSetsLoaded) @@ -422,9 +431,9 @@ namespace osu.Game.Screens.Select /// <returns>If a resultant action occurred that takes the user away from SongSelect.</returns> protected abstract bool OnStart(); - private ScheduledDelegate selectionChangedDebounce; + private ScheduledDelegate? selectionChangedDebounce; - private void updateCarouselSelection(ValueChangedEvent<WorkingBeatmap> e = null) + private void updateCarouselSelection(ValueChangedEvent<WorkingBeatmap>? e = null) { var beatmap = e?.NewValue ?? Beatmap.Value; if (beatmap is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; @@ -454,11 +463,11 @@ namespace osu.Game.Screens.Select } // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. - private BeatmapInfo beatmapInfoPrevious; - private BeatmapInfo beatmapInfoNoDebounce; - private RulesetInfo rulesetNoDebounce; + private BeatmapInfo? beatmapInfoPrevious; + private BeatmapInfo? beatmapInfoNoDebounce; + private RulesetInfo? rulesetNoDebounce; - private void updateSelectedBeatmap(BeatmapInfo beatmapInfo) + private void updateSelectedBeatmap(BeatmapInfo? beatmapInfo) { if (beatmapInfo == null && beatmapInfoNoDebounce == null) return; @@ -470,7 +479,7 @@ namespace osu.Game.Screens.Select performUpdateSelected(); } - private void updateSelectedRuleset(RulesetInfo ruleset) + private void updateSelectedRuleset(RulesetInfo? ruleset) { if (ruleset == null && rulesetNoDebounce == null) return; @@ -488,7 +497,7 @@ namespace osu.Game.Screens.Select private void performUpdateSelected() { var beatmap = beatmapInfoNoDebounce; - var ruleset = rulesetNoDebounce; + RulesetInfo? ruleset = rulesetNoDebounce; selectionChangedDebounce?.Cancel(); @@ -697,6 +706,7 @@ namespace osu.Game.Screens.Select isHandlingLooping = true; ensureTrackLooping(Beatmap.Value, TrackChangeDirection.None); + music.TrackChanged += ensureTrackLooping; } @@ -731,7 +741,7 @@ namespace osu.Game.Screens.Select decoupledRuleset.UnbindAll(); - if (music != null) + if (music.IsNotNull()) music.TrackChanged -= ensureTrackLooping; modSelectOverlayRegistration?.Dispose(); @@ -747,7 +757,7 @@ namespace osu.Game.Screens.Select ApplyToBackground(backgroundModeBeatmap => { backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.BlurAmount.Value = BACKGROUND_BLUR; + backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f; backgroundModeBeatmap.FadeColour(Color4.White, 250); }); @@ -766,7 +776,7 @@ namespace osu.Game.Screens.Select } } - private readonly WeakReference<ITrack> lastTrack = new WeakReference<ITrack>(null); + private readonly WeakReference<ITrack?> lastTrack = new WeakReference<ITrack?>(null); /// <summary> /// Ensures some music is playing for the current track. @@ -867,18 +877,19 @@ namespace osu.Game.Screens.Select // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). - Carousel?.FlushPendingFilterOperations(); + Carousel.FlushPendingFilterOperations(); + return true; } - private void delete(BeatmapSetInfo beatmap) + private void delete(BeatmapSetInfo? beatmap) { if (beatmap == null) return; dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } - private void clearScores(BeatmapInfo beatmapInfo) + private void clearScores(BeatmapInfo? beatmapInfo) { if (beatmapInfo == null) return; @@ -953,7 +964,7 @@ namespace osu.Game.Screens.Select private partial class ResetScrollContainer : Container { - private readonly Action onHoverAction; + private readonly Action? onHoverAction; public ResetScrollContainer(Action onHoverAction) { diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index d78147aaea..53c6c1e5ce 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -108,6 +108,7 @@ namespace osu.Game.Skinning var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault(); var combo = container.OfType<DefaultComboCounter>().FirstOrDefault(); var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault(); + var songProgress = container.OfType<ArgonSongProgress>().FirstOrDefault(); if (score != null) { @@ -158,6 +159,12 @@ namespace osu.Game.Skinning // origin flipped to match scale above. hitError2.Origin = Anchor.CentreLeft; } + + if (songProgress != null) + { + songProgress.Position = new Vector2(0, -10); + songProgress.Scale = new Vector2(0.9f, 1); + } } }) { @@ -167,7 +174,7 @@ namespace osu.Game.Skinning new DefaultScoreCounter(), new DefaultAccuracyCounter(), new DefaultHealthDisplay(), - new DefaultSongProgress(), + new ArgonSongProgress(), new BarHitErrorMeter(), new BarHitErrorMeter(), new PerformancePointsCounter() diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 8361619574..68bb1e7ddc 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -11,12 +11,11 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; -using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -24,10 +23,8 @@ using osu.Game.Resources.Localisation.Web; namespace osu.Game.Skinning.Components { [UsedImplicitly] - public partial class BeatmapAttributeText : Container, ISkinnableDrawable + public partial class BeatmapAttributeText : FontAdjustableSkinComponent { - public bool UsesFixedAnchor { get; set; } - [SettingSource("Attribute", "The attribute to be displayed.")] public Bindable<BeatmapAttribute> Attribute { get; } = new Bindable<BeatmapAttribute>(BeatmapAttribute.StarRating); @@ -67,7 +64,6 @@ namespace osu.Game.Skinning.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.Default.With(size: 40) } }; } @@ -122,6 +118,8 @@ namespace osu.Game.Skinning.Components text.Text = LocalisableString.Format(numberedTemplate, args); } + + protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); } public enum BeatmapAttribute diff --git a/osu.Game/Skinning/Components/BigBlackBox.cs b/osu.Game/Skinning/Components/BigBlackBox.cs index 4210a70b72..043a276e49 100644 --- a/osu.Game/Skinning/Components/BigBlackBox.cs +++ b/osu.Game/Skinning/Components/BigBlackBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index 74a0acb979..d87fb125bb 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -12,17 +12,16 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Skinning.Components { [UsedImplicitly] - public partial class TextElement : Container, ISkinnableDrawable + public partial class TextElement : FontAdjustableSkinComponent { - public bool UsesFixedAnchor { get; set; } - [SettingSource("Text", "The text to be displayed.")] public Bindable<string> Text { get; } = new Bindable<string>("Circles!"); + private readonly OsuSpriteText text; + public TextElement() { AutoSizeAxes = Axes.Both; - OsuSpriteText text; InternalChildren = new Drawable[] { text = new OsuSpriteText @@ -34,5 +33,7 @@ namespace osu.Game.Skinning.Components }; text.Current.BindTo(Text); } + + protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); } } diff --git a/osu.Game/Skinning/Editor/SkinBlueprint.cs b/osu.Game/Skinning/Editor/SkinBlueprint.cs index fc7e9e8ef1..c5e822a17a 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprint.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -20,18 +18,18 @@ namespace osu.Game.Skinning.Editor { public partial class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable> { - private Container box; + private Container box = null!; - private Container outlineBox; + private Container outlineBox = null!; - private AnchorOriginVisualiser anchorOriginVisualiser; + private AnchorOriginVisualiser anchorOriginVisualiser = null!; private Drawable drawable => (Drawable)Item; protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; public SkinBlueprint(ISkinnableDrawable component) : base(component) diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 9812406aad..2998136aca 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -28,7 +26,7 @@ namespace osu.Game.Skinning.Editor private readonly List<BindableList<ISkinnableDrawable>> targetComponents = new List<BindableList<ISkinnableDrawable>>(); [Resolved] - private SkinEditor editor { get; set; } + private SkinEditor editor { get; set; } = null!; public SkinBlueprintContainer(Drawable target) { @@ -61,7 +59,7 @@ namespace osu.Game.Skinning.Editor } } - private void componentsChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) { diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs index 053119557f..9985b81059 100644 --- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs @@ -10,6 +10,7 @@ using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play.HUD; @@ -26,7 +27,7 @@ namespace osu.Game.Skinning.Editor private FillFlowContainer fill = null!; public SkinComponentToolbox(CompositeDrawable? target = null) - : base("Components") + : base(SkinEditorStrings.Components) { this.target = target; } diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 0ed4e5afd2..5c0c8b2427 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -42,39 +41,39 @@ namespace osu.Game.Skinning.Editor protected override bool StartHidden => true; - private Drawable targetScreen; + private Drawable targetScreen = null!; - private OsuTextFlowContainer headerText; + private OsuTextFlowContainer headerText = null!; - private Bindable<Skin> currentSkin; - - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } + private Bindable<Skin> currentSkin = null!; [Resolved] - private SkinManager skins { get; set; } + private OsuGame? game { get; set; } [Resolved] - private OsuColour colours { get; set; } + private SkinManager skins { get; set; } = null!; [Resolved] - private RealmAccess realm { get; set; } + private OsuColour colours { get; set; } = null!; - [Resolved(canBeNull: true)] - private SkinEditorOverlay skinEditorOverlay { get; set; } + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private SkinEditorOverlay? skinEditorOverlay { get; set; } [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); private bool hasBegunMutating; - private Container content; + private Container? content; - private EditorSidebar componentsSidebar; - private EditorSidebar settingsSidebar; + private EditorSidebar componentsSidebar = null!; + private EditorSidebar settingsSidebar = null!; - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } public SkinEditor() { @@ -109,7 +108,7 @@ namespace osu.Game.Skinning.Editor { new Container { - Name = "Menu container", + Name = @"Menu container", RelativeSizeAxes = Axes.X, Depth = float.MinValue, Height = MENU_HEIGHT, @@ -122,14 +121,14 @@ namespace osu.Game.Skinning.Editor RelativeSizeAxes = Axes.Both, Items = new[] { - new MenuItem("File") + new MenuItem(CommonStrings.MenuBarFile) { Items = new[] { - new EditorMenuItem("Save", MenuItemType.Standard, Save), - new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert), + new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, Save), + new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, revert), new EditorMenuItemSpacer(), - new EditorMenuItem("Exit", MenuItemType.Standard, () => skinEditorOverlay?.Hide()), + new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), }, }, } @@ -240,6 +239,8 @@ namespace osu.Game.Skinning.Editor void loadBlueprintContainer() { + Debug.Assert(content != null); + content.Child = new SkinBlueprintContainer(targetScreen); componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable) @@ -253,7 +254,7 @@ namespace osu.Game.Skinning.Editor { headerText.Clear(); - headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 16)); + headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16)); headerText.NewParagraph(); headerText.AddText("Currently editing ", cp => { @@ -312,9 +313,9 @@ namespace osu.Game.Skinning.Editor private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>(); - private ISkinnableTarget getFirstTarget() => availableTargets.FirstOrDefault(); + private ISkinnableTarget? getFirstTarget() => availableTargets.FirstOrDefault(); - private ISkinnableTarget getTarget(GlobalSkinComponentLookup.LookupType target) + private ISkinnableTarget? getTarget(GlobalSkinComponentLookup.LookupType target) { return availableTargets.FirstOrDefault(c => c.Target == target); } @@ -328,7 +329,7 @@ namespace osu.Game.Skinning.Editor currentSkin.Value.ResetDrawableTarget(t); // add back default components - getTarget(t.Target).Reload(); + getTarget(t.Target)?.Reload(); } } @@ -343,7 +344,7 @@ namespace osu.Game.Skinning.Editor currentSkin.Value.UpdateDrawableTarget(t); skins.Save(skins.CurrentSkin.Value); - onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString())); + onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString() ?? "Unknown")); } protected override bool OnHover(HoverEvent e) => true; @@ -395,12 +396,18 @@ namespace osu.Game.Skinning.Editor // This is the best we can do for now. realm.Run(r => r.Refresh()); + var skinnableTarget = getFirstTarget(); + + // Import still should happen for now, even if not placeable (as it allows a user to import skin resources that would apply to legacy gameplay skins). + if (skinnableTarget == null) + return; + // place component var sprite = new SkinnableSprite { SpriteName = { Value = file.Name }, Origin = Anchor.Centre, - Position = getFirstTarget().ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position), + Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position), }; placeComponent(sprite, false); diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index 35e28ee665..f15afc7feb 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Diagnostics; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -29,13 +26,12 @@ namespace osu.Game.Skinning.Editor protected override bool BlockNonPositionalInput => true; - [CanBeNull] - private SkinEditor skinEditor; + private SkinEditor? skinEditor; - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } + [Resolved] + private OsuGame game { get; set; } = null!; - private OsuScreen lastTargetScreen; + private OsuScreen? lastTargetScreen; private Vector2 lastDrawSize; @@ -81,6 +77,8 @@ namespace osu.Game.Skinning.Editor AddInternal(editor); + Debug.Assert(lastTargetScreen != null); + SetTarget(lastTargetScreen); }); } @@ -124,15 +122,15 @@ namespace osu.Game.Skinning.Editor { Scheduler.AddOnce(updateScreenSizing); - game?.Toolbar.Hide(); - game?.CloseAllOverlays(); + game.Toolbar.Hide(); + game.CloseAllOverlays(); } else { scalingContainer.SetCustomRect(null); if (lastTargetScreen?.HideOverlaysOnEnter != true) - game?.Toolbar.Show(); + game.Toolbar.Show(); } } @@ -158,7 +156,7 @@ namespace osu.Game.Skinning.Editor Scheduler.AddOnce(setTarget, screen); } - private void setTarget(OsuScreen target) + private void setTarget(OsuScreen? target) { if (target == null) return; diff --git a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs index c3907ea60d..0936be3ee8 100644 --- a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -35,14 +33,14 @@ namespace osu.Game.Skinning.Editor private const float padding = 10; - [Resolved(canBeNull: true)] - private IPerformFromScreenRunner performer { get; set; } + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } [Resolved] - private IBindable<RulesetInfo> ruleset { get; set; } + private IBindable<RulesetInfo> ruleset { get; set; } = null!; [Resolved] - private Bindable<IReadOnlyList<Mod>> mods { get; set; } + private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!; public SkinEditorSceneLibrary() { @@ -66,7 +64,7 @@ namespace osu.Game.Skinning.Editor { new FillFlowContainer { - Name = "Scene library", + Name = @"Scene library", AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, Spacing = new Vector2(padding), @@ -76,14 +74,14 @@ namespace osu.Game.Skinning.Editor { new OsuSpriteText { - Text = "Scene library", + Text = SkinEditorStrings.SceneLibrary, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding(10), }, new SceneButton { - Text = "Song Select", + Text = SkinEditorStrings.SongSelect, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Action = () => performer?.PerformFromScreen(screen => @@ -96,7 +94,7 @@ namespace osu.Game.Skinning.Editor }, new SceneButton { - Text = "Gameplay", + Text = SkinEditorStrings.Gameplay, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Action = () => performer?.PerformFromScreen(screen => @@ -106,7 +104,12 @@ namespace osu.Game.Skinning.Editor var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); - if (!ModUtils.CheckCompatibleSet(mods.Value.Append(replayGeneratingMod), out var invalid)) + IReadOnlyList<Mod> usableMods = mods.Value; + + if (replayGeneratingMod != null) + usableMods = usableMods.Append(replayGeneratingMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) mods.Value = mods.Value.Except(invalid).ToArray(); if (replayGeneratingMod != null) @@ -128,8 +131,8 @@ namespace osu.Game.Skinning.Editor Height = BUTTON_HEIGHT; } - [BackgroundDependencyLoader(true)] - private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) { BackgroundColour = overlayColourProvider?.Background3 ?? colours.Blue3; Content.CornerRadius = 5; diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 2c3f6238ec..d67cbef481 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -23,7 +21,7 @@ namespace osu.Game.Skinning.Editor public partial class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable> { [Resolved] - private SkinEditor skinEditor { get; set; } + private SkinEditor skinEditor { get; set; } = null!; public override bool HandleRotation(float angle) { diff --git a/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs b/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs index 8c08f40b70..6afe92c6b2 100644 --- a/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs @@ -1,12 +1,11 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Screens.Edit.Components; using osuTK; @@ -17,7 +16,7 @@ namespace osu.Game.Skinning.Editor protected override Container<Drawable> Content { get; } public SkinSettingsToolbox(Drawable component) - : base($"Settings ({component.GetType().Name})") + : base(SkinEditorStrings.Settings(component.GetType().Name)) { base.Content.Add(Content = new FillFlowContainer { diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs new file mode 100644 index 0000000000..ee73417bfe --- /dev/null +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Graphics; + +namespace osu.Game.Skinning +{ + /// <summary> + /// A skin component that contains text and allows the user to choose its font. + /// </summary> + public abstract partial class FontAdjustableSkinComponent : Container, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Font", "The font to use.")] + public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus); + + /// <summary> + /// Implement to apply the user font selection to one or more components. + /// </summary> + protected abstract void SetFont(FontUsage font); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Font.BindValueChanged(e => + { + // We only have bold weight for venera, so let's force that. + FontWeight fontWeight = e.NewValue == Typeface.Venera ? FontWeight.Bold : FontWeight.Regular; + + FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight); + SetFont(f); + }, true); + } + } +} diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 0223e7a5a2..796080f4f5 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -65,11 +65,14 @@ namespace osu.Game.Skinning default: this.ScaleTo(0.6f).Then() - .ScaleTo(1.1f, fade_in_length * 0.8f).Then() - // this is actually correct to match stable; there were overlapping transforms. - .ScaleTo(0.9f).Delay(fade_in_length * 0.2f) - .ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then() - .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); + .ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8 + .Delay(fade_in_length * 0.2f) // t = 1.0 + .ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2 + + // stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2. + // so we need to force the current value to be correct at 1.2 (0.95) then complete the + // second half of the transform. + .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4 break; } } diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs index 465f1c3a20..8282b2f88b 100644 --- a/osu.Game/Skinning/LegacyRollingCounter.cs +++ b/osu.Game/Skinning/LegacyRollingCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 6a14ed93e9..88e7bbc23a 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs index 11c21d432f..1270f69339 100644 --- a/osu.Game/Skinning/LegacySkinDecoder.cs +++ b/osu.Game/Skinning/LegacySkinDecoder.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Globalization; using osu.Game.Beatmaps.Formats; diff --git a/osu.Game/Skinning/LegacySongProgress.cs b/osu.Game/Skinning/LegacySongProgress.cs index 10d1431ed4..22aea99291 100644 --- a/osu.Game/Skinning/LegacySongProgress.cs +++ b/osu.Game/Skinning/LegacySongProgress.cs @@ -15,6 +15,10 @@ namespace osu.Game.Skinning { private CircularProgress circularProgress = null!; + // Legacy song progress doesn't support interaction for now. + public override bool HandleNonPositionalInput => false; + public override bool HandlePositionalInput => false; + [BackgroundDependencyLoader] private void load() { @@ -56,16 +60,6 @@ namespace osu.Game.Skinning }; } - protected override void PopIn() - { - this.FadeIn(500, Easing.OutQuint); - } - - protected override void PopOut() - { - this.FadeOut(100); - } - protected override void UpdateProgress(double progress, bool isIntro) { if (isIntro) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index b89f85778b..d6af52855b 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -16,7 +14,7 @@ namespace osu.Game.Skinning { private readonly LegacyFont font; - private LegacyGlyphStore glyphStore; + private LegacyGlyphStore glyphStore = null!; protected override char FixedWidthReferenceCharacter => '5'; @@ -49,7 +47,7 @@ namespace osu.Game.Skinning this.skin = skin; } - public ITexturedCharacterGlyph Get(string fontName, char character) + public ITexturedCharacterGlyph? Get(string fontName, char character) { string lookup = getLookupName(character); @@ -79,7 +77,7 @@ namespace osu.Game.Skinning } } - public Task<ITexturedCharacterGlyph> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + public Task<ITexturedCharacterGlyph?> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); } } } diff --git a/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs b/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs new file mode 100644 index 0000000000..f15097a169 --- /dev/null +++ b/osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace osu.Game.Skinning +{ + public class MaxDimensionLimitedTextureLoaderStore : IResourceStore<TextureUpload> + { + private readonly IResourceStore<TextureUpload>? textureStore; + + public MaxDimensionLimitedTextureLoaderStore(IResourceStore<TextureUpload>? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return limitTextureUploadSize(textureUpload); + } + + public async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload limitTextureUploadSize(TextureUpload textureUpload) + { + // So there's a thing where some users have taken it upon themselves to create skin elements of insane dimensions. + // To the point where GPUs cannot load the textures (along with most image editor apps). + // To work around this, let's look out for any stupid images and shrink them down into a usable size. + const int max_supported_texture_size = 8192; + + if (textureUpload.Height > max_supported_texture_size || textureUpload.Width > max_supported_texture_size) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + image.Mutate(i => i.Resize(new Size( + Math.Min(textureUpload.Width, max_supported_texture_size), + Math.Min(textureUpload.Height, max_supported_texture_size) + ))); + + return new TextureUpload(image); + } + + return textureUpload; + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>(); + } +} diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 19e8bc7092..37e98946c9 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -69,15 +69,18 @@ namespace osu.Game.Skinning storage ??= realmBackedStorage = new RealmBackedResourceStore<SkinInfo>(SkinInfo, resources.Files, resources.RealmAccess); var samples = resources.AudioManager?.GetSampleStore(storage); + if (samples != null) + { samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - // osu-stable performs audio lookups in order of wav -> mp3 -> ogg. - // The GetSampleStore() call above internally adds wav and mp3, so ogg is added at the end to ensure expected ordering. - (storage as ResourceStore<byte[]>)?.AddExtension("ogg"); + // osu-stable performs audio lookups in order of wav -> mp3 -> ogg. + // The GetSampleStore() call above internally adds wav and mp3, so ogg is added at the end to ensure expected ordering. + samples.AddExtension(@"ogg"); + } Samples = samples; - Textures = new TextureStore(resources.Renderer, resources.CreateTextureLoaderStore(storage)); + Textures = new TextureStore(resources.Renderer, new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage))); } else { diff --git a/osu.Game/Skinning/SkinCustomColourLookup.cs b/osu.Game/Skinning/SkinCustomColourLookup.cs index 319b071bf5..b8e5ac9b53 100644 --- a/osu.Game/Skinning/SkinCustomColourLookup.cs +++ b/osu.Game/Skinning/SkinCustomColourLookup.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Skinning { public class SkinCustomColourLookup diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index aeced9b517..475b79053a 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -39,13 +36,12 @@ namespace osu.Game.Skinning /// <summary> /// All raw <see cref="DrawableSamples"/>s contained in this <see cref="SkinnableSound"/>. /// </summary> - [NotNull, ItemNotNull] protected IEnumerable<DrawableSample> DrawableSamples => samplesContainer.Select(c => c.Sample).Where(s => s != null); private readonly AudioContainer<PoolableSkinnableSample> samplesContainer; - [Resolved(CanBeNull = true)] - private IPooledSampleProvider samplePool { get; set; } + [Resolved] + private IPooledSampleProvider? samplePool { get; set; } /// <summary> /// Creates a new <see cref="SkinnableSound"/>. @@ -59,7 +55,7 @@ namespace osu.Game.Skinning /// Creates a new <see cref="SkinnableSound"/> with some initial samples. /// </summary> /// <param name="samples">The initial samples.</param> - public SkinnableSound([NotNull] IEnumerable<ISampleInfo> samples) + public SkinnableSound(IEnumerable<ISampleInfo> samples) : this() { this.samples = samples.ToArray(); @@ -69,7 +65,7 @@ namespace osu.Game.Skinning /// Creates a new <see cref="SkinnableSound"/> with an initial sample. /// </summary> /// <param name="sample">The initial sample.</param> - public SkinnableSound([NotNull] ISampleInfo sample) + public SkinnableSound(ISampleInfo sample) : this(new[] { sample }) { } @@ -84,8 +80,6 @@ namespace osu.Game.Skinning get => samples; set { - value ??= Array.Empty<ISampleInfo>(); - if (samples == value) return; @@ -96,6 +90,8 @@ namespace osu.Game.Skinning } } + public void ClearSamples() => Samples = Array.Empty<ISampleInfo>(); + private bool looping; /// <summary> diff --git a/osu.Game/Tests/OsuTestBrowser.cs b/osu.Game/Tests/OsuTestBrowser.cs index 7431679ab9..689eae336e 100644 --- a/osu.Game/Tests/OsuTestBrowser.cs +++ b/osu.Game/Tests/OsuTestBrowser.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests { Depth = 10, RelativeSizeAxes = Axes.Both, - }, AddInternal); + }, Add); // Have to construct this here, rather than in the constructor, because // we depend on some dependencies to be loaded within OsuGameBase.load(). diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index b0043b902c..a5e0bddc6b 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -50,17 +50,16 @@ namespace osu.Game.Tests.Visual { var cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both }; - cursorDisplay.Add(new OsuTooltipContainer(cursorDisplay.MenuCursor) + cursorDisplay.Add(content = new OsuTooltipContainer(cursorDisplay.MenuCursor) { RelativeSizeAxes = Axes.Both, - Child = mainContent }); - mainContent = cursorDisplay; + mainContent.Add(cursorDisplay); } if (CreateNestedActionContainer) - mainContent = new GlobalActionContainer(null).WithChild(mainContent); + mainContent.Add(new GlobalActionContainer(null)); base.Content.AddRange(new Drawable[] { diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 3ecc5a3280..0392e3ae52 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -25,6 +26,24 @@ namespace osu.Game.Tests.Visual protected OsuConfigManager LocalConfig; + private double lastReportedTime; + + protected override void Update() + { + base.Update(); + + if (Player?.GameplayClockContainer != null) + { + int roundedTime = (int)Player.GameplayClockContainer.CurrentTime / 1000; + + if (roundedTime != lastReportedTime) + { + lastReportedTime = roundedTime; + Logger.Log($"⏱️ Gameplay clock reached {lastReportedTime * 1000:N0} ms"); + } + } + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index e8f51f9afa..aab1b72990 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual { RelativeSizeAxes = Axes.Both, BorderColour = Color4.White, - BorderThickness = 5, + BorderThickness = 3, Masking = true, Children = new Drawable[] @@ -142,8 +142,15 @@ namespace osu.Game.Tests.Visual c.AutoSizeAxes = Axes.None; c.Size = Vector2.Zero; - c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None; - c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None; + if (autoSize) + c.AutoSizeAxes = Axes.Both; + else + { + c.RelativeSizeAxes = Axes.Both; + c.Anchor = Anchor.Centre; + c.Origin = Anchor.Centre; + c.Size = new Vector2(0.97f); + } } outlineBox.Alpha = autoSize ? 1 : 0; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cce3e42be4..d4ea0abbb5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Realm" Version="10.18.0" /> - <PackageReference Include="ppy.osu.Framework" Version="2022.1226.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2023.131.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1221.0" /> <PackageReference Include="Sentry" Version="3.23.1" /> <PackageReference Include="SharpCompress" Version="0.32.2" /> diff --git a/osu.iOS.props b/osu.iOS.props index 6201022da1..3fccbbe60c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -16,6 +16,6 @@ <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> </PropertyGroup> <ItemGroup> - <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1226.0" /> + <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.131.0" /> </ItemGroup> </Project>