diff --git a/Gemfile.lock b/Gemfile.lock index 86c8baabe6..1010027af9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,17 +8,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.551.0) - aws-sdk-core (3.125.5) + aws-partitions (1.553.0) + aws-sdk-core (3.126.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (1.54.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.111.3) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-s3 (1.112.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -27,8 +27,8 @@ GEM claide (1.1.0) colored (1.2) colored2 (3.1.2) - commander-fastlane (4.4.6) - highline (~> 1.7.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) @@ -36,7 +36,7 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.3) - excon (0.90.0) + excon (0.91.0) faraday (1.9.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -66,15 +66,15 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.181.0) + fastlane (2.204.2) CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.3, < 3.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-fastlane (>= 4.4.6, < 5.0.0) + commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -83,19 +83,20 @@ GEM faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.37.0, < 0.39.0) - google-cloud-storage (>= 1.15.0, < 2.0.0) - highline (>= 1.7.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) - slack-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) @@ -105,18 +106,12 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-clean_testflight_testers (0.3.0) - fastlane-plugin-souyuz (0.9.1) - souyuz (= 0.9.1) + fastlane-plugin-souyuz (0.11.1) + souyuz (= 0.11.1) fastlane-plugin-xamarin (0.6.3) gh_inspector (1.1.3) - google-api-client (0.38.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 0.9) - httpclient (>= 2.8.1, < 3.0) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.0) - signet (~> 0.12) + google-apis-androidpublisher_v3 (0.16.0) + google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.2) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) @@ -128,6 +123,8 @@ GEM webrick google-apis-iamcredentials_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) + google-apis-playcustomapp_v1 (0.7.0) + google-apis-core (>= 0.4, < 2.a) google-apis-storage_v1 (0.11.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) @@ -144,14 +141,14 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (0.17.1) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.15) - highline (1.7.10) + signet (>= 0.16, < 2.a) + highline (2.0.3) http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) @@ -161,16 +158,19 @@ GEM memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.4.0) + mini_portile2 (2.7.1) multi_json (1.15.0) multipart-post (2.0.0) nanaimo (0.3.0) naturally (2.2.1) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) + nokogiri (1.13.1) + mini_portile2 (~> 2.7.0) + racc (~> 1.4) + optparse (0.1.1) os (1.1.4) plist (3.6.0) public_suffix (4.0.6) + racc (1.6.0) rake (13.0.6) representable (3.1.1) declarative (< 0.1.0) @@ -190,10 +190,9 @@ GEM simctl (1.6.8) CFPropertyList naturally - slack-notifier (2.4.0) - souyuz (0.9.1) - fastlane (>= 1.103.0) - highline (~> 1.7) + souyuz (0.11.1) + fastlane (>= 2.182.0) + highline (~> 2.0) nokogiri (~> 1.7) terminal-notifier (2.0.0) terminal-table (1.8.0) diff --git a/Templates/Rulesets/ruleset-empty/.editorconfig b/Templates/Rulesets/ruleset-empty/.editorconfig index f3badda9b3..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-empty/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,4 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error diff --git a/Templates/Rulesets/ruleset-empty/.gitignore b/Templates/Rulesets/ruleset-empty/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-empty/.gitignore +++ b/Templates/Rulesets/ruleset-empty/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings index aa8f8739c1..9752e08599 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -717,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> @@ -909,26 +919,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj index cfe2bd1cb2..092a013614 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.EmptyFreeform Library AnyCPU osu.Game.Rulesets.EmptyFreeform diff --git a/Templates/Rulesets/ruleset-example/.editorconfig b/Templates/Rulesets/ruleset-example/.editorconfig index f3badda9b3..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-example/.editorconfig +++ b/Templates/Rulesets/ruleset-example/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,4 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error diff --git a/Templates/Rulesets/ruleset-example/.gitignore b/Templates/Rulesets/ruleset-example/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-example/.gitignore +++ b/Templates/Rulesets/ruleset-example/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index aa8f8739c1..9752e08599 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -717,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> @@ -909,26 +919,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj index 61b859f45b..a3607343c9 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.Pippidon Library AnyCPU osu.Game.Rulesets.Pippidon diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig index f3badda9b3..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-empty/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,4 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error diff --git a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/.gitignore +++ b/Templates/Rulesets/ruleset-scrolling-empty/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings index aa8f8739c1..9752e08599 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -717,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> @@ -909,26 +919,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj index 9dce3c9a0a..2ea52429ab 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.EmptyScrolling Library AnyCPU osu.Game.Rulesets.EmptyScrolling diff --git a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig index f3badda9b3..9c7537de4b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.editorconfig +++ b/Templates/Rulesets/ruleset-scrolling-example/.editorconfig @@ -10,14 +10,6 @@ trim_trailing_whitespace = true #Roslyn naming styles -#PascalCase for public and protected members -dotnet_naming_style.pascalcase.capitalization = pascal_case -dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected -dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event -dotnet_naming_rule.public_members_pascalcase.severity = error -dotnet_naming_rule.public_members_pascalcase.symbols = public_members -dotnet_naming_rule.public_members_pascalcase.style = pascalcase - #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -121,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning csharp_style_var_when_type_is_apparent = true:none -csharp_style_var_for_built_in_types = true:none +csharp_style_var_for_built_in_types = false:warning csharp_style_var_elsewhere = true:silent #Style - modifiers @@ -165,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning #Style - variable declaration csharp_style_inlined_variable_declaration = true:warning -csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_deconstructed_variable_declaration = false:silent #Style - other C# 7.x features dotnet_style_prefer_inferred_tuple_names = true:warning @@ -176,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent #Style - C# 8 features csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:silent -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_index_operator = false:silent +csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none #Supressing roslyn built-in analyzers @@ -197,4 +189,4 @@ dotnet_diagnostic.IDE0069.severity = none dotnet_diagnostic.CA2225.severity = none # Banned APIs -dotnet_diagnostic.RS0030.severity = error \ No newline at end of file +dotnet_diagnostic.RS0030.severity = error diff --git a/Templates/Rulesets/ruleset-scrolling-example/.gitignore b/Templates/Rulesets/ruleset-scrolling-example/.gitignore index 940794e60f..5b19270ab9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/.gitignore +++ b/Templates/Rulesets/ruleset-scrolling-example/.gitignore @@ -1,7 +1,5 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo @@ -17,8 +15,6 @@ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ bld/ [Bb]in/ [Oo]bj/ @@ -42,11 +38,10 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json *_i.c *_p.c @@ -113,10 +108,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -166,7 +157,7 @@ PublishScripts/ !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -196,10 +187,11 @@ ClientBin/ *~ *.dbmdl *.dbproj.schemaview -*.jfm *.pfx *.publishsettings +node_modules/ orleans.codegen.cs +Resource.designer.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) @@ -219,7 +211,6 @@ UpgradeLog*.htm # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data @@ -234,10 +225,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ # Visual Studio 6 build log *.plg @@ -245,9 +232,6 @@ typings/ # Visual Studio 6 workspace options file *.opt -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -263,26 +247,96 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# Cake # +/tools/** +/build/tools/** +/build/temp/** -# Telerik's JustMock configuration file -*.jmconfig +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd +**/FodyWeavers.xml diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings index aa8f8739c1..9752e08599 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.sln.DotSettings @@ -18,9 +18,10 @@ WARNING HINT DO_NOT_SHOW - HINT - WARNING - WARNING + WARNING + WARNING + HINT + HINT WARNING WARNING WARNING @@ -73,6 +74,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING @@ -105,8 +107,9 @@ HINT HINT WARNING + DO_NOT_SHOW + DO_NOT_SHOW WARNING - DO_NOT_SHOW WARNING WARNING WARNING @@ -120,6 +123,7 @@ WARNING WARNING HINT + HINT WARNING HINT HINT @@ -129,7 +133,7 @@ HINT WARNING WARNING - HINT + WARNING WARNING WARNING WARNING @@ -204,8 +208,10 @@ HINT WARNING WARNING - DO_NOT_SHOW + SUGGESTION DO_NOT_SHOW + + True DO_NOT_SHOW WARNING WARNING @@ -226,6 +232,7 @@ HINT DO_NOT_SHOW WARNING + WARNING WARNING WARNING WARNING @@ -298,15 +305,21 @@ True 200 CHOP_IF_LONG + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident False False AABB API BPM + EF + FPS GC GL GLSL HID + HSV HTML HUD ID @@ -717,9 +730,6 @@ </Group> </TypePattern> </Patterns> - 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. - <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> @@ -909,26 +919,82 @@ private void load() { $END$ }; + True True True + True True True True True + True + True + True + True + True + True + True + True True + True + True + True True + True + True + True True True + True + True + True + True + True True True + True + True True True + True + True + True + True + True + True + True + True True True + True + True + True + True + True + True + True + True True + True + True + True True True True + True + True + True + True + True + True True True - True + True + True + True + True + True + True + True + True + True diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj index 61b859f45b..a3607343c9 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj @@ -1,7 +1,7 @@  netstandard2.1 - osu.Game.Rulesets.Sample + osu.Game.Rulesets.Pippidon Library AnyCPU osu.Game.Rulesets.Pippidon diff --git a/Templates/osu.Game.Templates.csproj b/Templates/osu.Game.Templates.csproj index 31a24a301f..4624d3d771 100644 --- a/Templates/osu.Game.Templates.csproj +++ b/Templates/osu.Game.Templates.csproj @@ -15,6 +15,7 @@ true false content + true diff --git a/fastlane/README.md b/fastlane/README.md index 8273fdaa5d..9d5e11f7cb 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -1,78 +1,109 @@ fastlane documentation -================ +---- + # Installation Make sure you have the latest version of the Xcode command line tools installed: -``` +```sh xcode-select --install ``` -Install _fastlane_ using -``` -[sudo] gem install fastlane -NV -``` -or alternatively using `brew install fastlane` +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) # Available Actions + ## Android + ### android beta + +```sh +[bundle exec] fastlane android beta ``` -fastlane android beta -``` + Deploy to play store + ### android build_github + +```sh +[bundle exec] fastlane android build_github ``` -fastlane android build_github -``` + Deploy to github release + ### android build + +```sh +[bundle exec] fastlane android build ``` -fastlane android build -``` + Compile the project + ### android update_version + +```sh +[bundle exec] fastlane android update_version ``` -fastlane android update_version -``` + ---- + ## iOS + ### ios beta + +```sh +[bundle exec] fastlane ios beta ``` -fastlane ios beta -``` + Deploy to testflight + ### ios build + +```sh +[bundle exec] fastlane ios build ``` -fastlane ios build -``` + Compile the project + ### ios provision + +```sh +[bundle exec] fastlane ios provision ``` -fastlane ios provision -``` + Install provisioning profiles using match + ### ios update_version + +```sh +[bundle exec] fastlane ios update_version ``` -fastlane ios update_version -``` + + ### ios testflight_prune_dry -``` -fastlane ios testflight_prune_dry + +```sh +[bundle exec] fastlane ios testflight_prune_dry ``` + + ### ios testflight_prune + +```sh +[bundle exec] fastlane ios testflight_prune ``` -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). +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 f89994cd56..1a2859c851 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 25bd659a5d..2e83f784d3 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -27,7 +27,7 @@ namespace osu.Android { gameActivity.RunOnUiThread(() => { - gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index c9fb539d8a..eebd079f68 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,16 +9,18 @@ using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; -using Android.Net; +using Android.Graphics; using Android.OS; using Android.Provider; using Android.Views; using osu.Framework.Android; using osu.Game.Database; +using Debug = System.Diagnostics.Debug; +using Uri = Android.Net.Uri; namespace osu.Android { - [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser)] + [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] @@ -41,6 +44,12 @@ namespace osu.Android { private static readonly string[] osu_url_schemes = { "osu", "osump" }; + /// + /// The default screen orientation. + /// + /// Adjusted on startup to match expected UX for the current device type (phone/tablet). + public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; + private OsuGameAndroid game; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -54,8 +63,20 @@ namespace osu.Android // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) handleIntent(Intent); + Debug.Assert(Window != null); + Window.AddFlags(WindowManagerFlags.Fullscreen); Window.AddFlags(WindowManagerFlags.KeepScreenOn); + + Debug.Assert(WindowManager?.DefaultDisplay != null); + Debug.Assert(Resources?.DisplayMetrics != null); + + Point displaySize = new Point(); + WindowManager.DefaultDisplay.GetSize(displaySize); + float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; + bool isTablet = smallestWidthDp >= 600f; + + RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; } protected override void OnNewIntent(Intent intent) => handleIntent(intent); @@ -104,7 +125,7 @@ namespace osu.Android cursor.MoveToFirst(); - var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); + int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); string filename = cursor.GetString(filenameColumn); // SharpCompress requires archive streams to be seekable, which the stream opened by diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist index 3ba1886d98..33ddac6dfb 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Info.plist @@ -24,11 +24,16 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist index 09ed2dd007..78349334b4 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Info.plist @@ -24,11 +24,16 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs index da9634ba47..17c864a268 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -16,5 +17,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty [JsonProperty("scaled_score")] public double ScaledScore { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); + } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2098c7f5d8..180b9ef71b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -370,10 +370,10 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 + AutoSizeAxes = Axes.Y }), } }, @@ -381,10 +381,21 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(score.HitEvents) - })) + }), true) } } }; diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist index dd032ef1c1..b9f371c049 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Info.plist @@ -24,11 +24,16 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 6c7760d144..0aeaf7669f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -22,5 +23,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute(nameof(Aim), "Aim", Aim); + yield return new PerformanceDisplayAttribute(nameof(Speed), "Speed", Speed); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); + yield return new PerformanceDisplayAttribute(nameof(Flashlight), "Flashlight Bonus", Flashlight); + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 5d191119b9..1bf63ef6d4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -87,8 +87,8 @@ namespace osu.Game.Rulesets.Osu.Mods requiresHold |= slider.Ball.IsHovered || h.IsHovered; break; - case DrawableSpinner _: - requiresHold = true; + case DrawableSpinner spinner: + requiresHold |= spinner.HitObject.SpinsRequired > 0; break; } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index ec757c9f33..ad00a025a1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -279,22 +279,10 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", - new HitEventTimingDistributionGraph(timedHitEvents) - { - RelativeSizeAxes = Axes.X, - Height = 250 - }), - } - }, - new StatisticRow - { - Columns = new[] - { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 + AutoSizeAxes = Axes.Y }), } }, @@ -302,10 +290,32 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(timedHitEvents) - })) + }), true) } } }; diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist index ac658cd14e..65c47d2115 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Info.plist @@ -24,11 +24,16 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 0be005e1c4..eec88d7bf8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("failed", () => Player.HasFailed); + AddAssert("failed", () => Player.GameplayState.HasFailed); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 80552880ea..fa5c0202dd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Rulesets.Difficulty; @@ -13,5 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("accuracy")] public double Accuracy { get; set; } + + public override IEnumerable GetAttributesForDisplay() + { + foreach (var attribute in base.GetAttributesForDisplay()) + yield return attribute; + + yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); + yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy); + } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ca860f24c3..e56aabaf9d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -213,10 +213,10 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) + new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 250 + AutoSizeAxes = Axes.Y }), } }, @@ -224,10 +224,21 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }, true), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new UnstableRate(timedHitEvents) - })) + }), true) } } }; diff --git a/osu.Game.Tests.iOS/Info.plist b/osu.Game.Tests.iOS/Info.plist index 1a89345bc5..ed0c2e4dbf 100644 --- a/osu.Game.Tests.iOS/Info.plist +++ b/osu.Game.Tests.iOS/Info.plist @@ -24,11 +24,16 @@ armv7 UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 94e61eaee0..343fc7e6e0 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -92,7 +92,8 @@ namespace osu.Game.Tests.Online AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); - addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); + AddAssert("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet)); + addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index e3fb44534b..a14c9aded3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -90,5 +90,100 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); } + + [Test] + public void TestCreateNewDifficulty() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + string secondDifficultyName = Guid.NewGuid().ToString(); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == firstDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID); + }); + AddAssert("can save again", () => Editor.Save()); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName); + var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID); + + return beatmap != null + && beatmap.DifficultyName == secondDifficultyName + && set != null + && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + }); + } + + [Test] + public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties() + { + Guid setId = Guid.Empty; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddAssert("beatmap set unchanged", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + } + + [Test] + public void TestCreateNewBeatmapFailsWithSameNamedDifficulties() + { + Guid setId = Guid.Empty; + const string duplicate_difficulty_name = "duplicate"; + + AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for created", () => + { + string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != duplicate_difficulty_name; + }); + + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); + AddStep("try to save beatmap", () => Editor.Save()); + AddAssert("beatmap set not corrupted", () => + { + var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); + // the difficulty was already created at the point of the switch. + // what we want to check is that both difficulties do not use the same file. + return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2); + }); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 7167d3120a..744227c55e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); // The pause screen and fail animation both ramp frequency. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index fa27e1abdd..6430c29dfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddAssert("total number of results == 1", () => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 04676f656f..ea0255ab76 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseAfterFail() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayAfterFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); confirmClockRunning(false); @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromFailedGameplayDuringFailAnimation() { - AddUntilStep("wait for fail", () => Player.HasFailed); + 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()); @@ -227,7 +227,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetryFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -236,7 +236,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromFailedGameplay() { - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); @@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay { confirmClockRunning(false); confirmNotExited(); - AddAssert("player not failed", () => !Player.HasFailed); + AddAssert("player not failed", () => !Player.GameplayState.HasFailed); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index a9675a2ee2..58b5df2612 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for token request", () => Player.TokenCreationRequested); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure no submission", () => Player.SubmittedScore == null); @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddStep("exit", () => Player.Exit()); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a7b9d45f7a..157c248d69 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -155,11 +155,13 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); + sendFrames(); - finish(); + finish(SpectatedUserState.Failed); - checkPaused(false); - // TODO: should replay until running out of frames then fail + checkPaused(false); // Should continue playing until out of frames + checkPaused(true); // And eventually stop after running out of frames and fail. + // Todo: Should check for + display a failed message. } [Test] @@ -211,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send frames and finish play", () => { spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero)); - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true }); }); // We can't access API because we're an "online" test. @@ -234,6 +236,71 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null); } + [Test] + public void TestPlayingState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + + [Test] + public void TestPassedState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed)); + AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + + [Test] + public void TestQuitState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id)); + AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + + [Test] + public void TestFailedState() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + + start(); + sendFrames(); + waitForPlayer(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; @@ -246,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); - private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id)); + private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state)); private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 409cec4cf6..034519fbf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClientSendsCorrectRuleset() { - AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id)); - AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); + AddUntilStep("spectator client sending frames", () => spectatorClient.WatchedUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); } public override void TearDownSteps() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4af254866a..a4d8460846 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; 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.Graphics.Shapes; @@ -20,7 +16,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -32,6 +27,7 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Tests.Visual.Spectator; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -40,145 +36,110 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene { - protected override bool UseOnlineAPI => true; - private TestRulesetInputManager playbackManager; private TestRulesetInputManager recordingManager; private Replay replay; - private readonly IBindableList users = new BindableList(); - - private TestReplayRecorder recorder; - private ManualClock manualClock; private OsuSpriteText latencyDisplay; private TestFramedReplayInputHandler replayHandler; - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private SpectatorClient spectatorClient { get; set; } - - [Cached] - private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); - [SetUpSteps] public void SetUpSteps() { - AddStep("Reset recorder state", cleanUpState); - AddStep("Setup containers", () => { replay = new Replay(); manualClock = new ManualClock(); + SpectatorClient spectatorClient; + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] + { + (typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())), + (typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty())) + }, + Children = new Drawable[] + { + spectatorClient, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Recorder = new TestReplayRecorder + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Sending", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + }, + new Drawable[] + { + playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Clock = new FramedClock(manualClock), + ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) + { + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Receiving", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + } + } + }, + latencyDisplay = new OsuSpriteText() + } + }; spectatorClient.OnNewFrames += onNewFrames; - - users.BindTo(spectatorClient.PlayingUsers); - users.BindCollectionChanged((obj, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null); - - foreach (int user in args.NewItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.WatchUser(user); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null); - - foreach (int user in args.OldItems) - { - if (user == api.LocalUser.Value.Id) - spectatorClient.StopWatchingUser(user); - } - - break; - } - }, true); - - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Recorder = recorder = new TestReplayRecorder - { - ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.Brown, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Sending", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - }, - new Drawable[] - { - playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique) - { - Clock = new FramedClock(manualClock), - ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) - { - GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), - }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkBlue, - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = "Receiving", - Scale = new Vector2(3), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new TestInputConsumer() - } - }, - } - } - } - }, - latencyDisplay = new OsuSpriteText() - }; }); } @@ -238,20 +199,6 @@ namespace osu.Game.Tests.Visual.Gameplay manualClock.CurrentTime = time.Value; } - [TearDownSteps] - public void TearDown() - { - AddStep("stop recorder", cleanUpState); - } - - private void cleanUpState() - { - // Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`. - recorder?.RemoveAndDisposeImmediately(); - recorder = null; - spectatorClient.OnNewFrames -= onNewFrames; - } - public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 69798dcb82..b87183cbc7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); - AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3563869d8b..8f6ba6375f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -871,6 +871,53 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0); } + [Test] + public void TestGameplayStartsWhileInSpectatorScreen() + { + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + } + } + }); + + AddStep("join other user and make host", () => + { + client.AddUser(new APIUser { Id = 1234 }); + client.TransferHost(1234); + }); + + AddStep("set local user spectating", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("wait for spectating state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); + + runGameplay(); + + AddStep("exit gameplay for other user", () => client.ChangeUserState(1234, MultiplayerUserState.Idle)); + AddUntilStep("wait for room to be idle", () => client.Room?.State == MultiplayerRoomState.Open); + + runGameplay(); + + void runGameplay() + { + AddStep("start match by other user", () => + { + client.ChangeUserState(1234, MultiplayerUserState.Ready); + client.StartMatch(); + }); + + AddUntilStep("wait for loading", () => client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddStep("set player loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); + AddUntilStep("wait for gameplay to start", () => client.Room?.State == MultiplayerRoomState.Playing); + AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen); + } + } + private void enterGameplay() { pressReadyButton(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 9b8e67b07a..1322fbc96e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RandomlyUpdateState() { - foreach (int userId in PlayingUsers) + foreach ((int userId, _) in WatchedUserStates) { if (RNG.NextBool()) continue; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index e31377b96e..8debb95f38 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for fail", () => player.HasFailed); + AddUntilStep("wait for fail", () => player.GameplayState.HasFailed); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 988f429ff5..167acc94c4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("click to right of panel", () => { var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); - InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0)); + InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(50, 0)); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f64b7b2b65..35281a85eb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -6,10 +6,18 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; using osuTK; @@ -41,6 +49,24 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(TestResources.CreateTestScoreInfo()); } + [Test] + public void TestScoreInRulesetWhereAllStatsRequireHitEvents() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetAllStatsRequireHitEvents().RulesetInfo)); + } + + [Test] + public void TestScoreInRulesetWhereNoStatsRequireHitEvents() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetNoStatsRequireHitEvents().RulesetInfo)); + } + + [Test] + public void TestScoreInMixedRuleset() + { + loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetMixed().RulesetInfo)); + } + [Test] public void TestNullScore() { @@ -75,5 +101,134 @@ namespace osu.Game.Tests.Visual.Ranking return hitEvents; } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) + { + throw new NotImplementedException(); + } + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) + { + throw new NotImplementedException(); + } + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) + { + throw new NotImplementedException(); + } + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) + { + throw new NotImplementedException(); + } + + public override string Description => string.Empty; + + public override string ShortName => string.Empty; + + protected static Drawable CreatePlaceholderStatistic(string message) => new Container + { + RelativeSizeAxes = Axes.X, + Masking = true, + CornerRadius = 20, + Height = 250, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.5f), + Alpha = 0.5f + }, + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Text = message, + Margin = new MarginPadding { Left = 20 } + } + } + }; + } + + private class TestRulesetAllStatsRequireHitEvents : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events 1", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events 2", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + } + }; + } + } + + private class TestRulesetNoStatsRequireHitEvents : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events 1", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events 2", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + } + }; + } + } + + private class TestRulesetMixed : TestRuleset + { + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + { + return new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Requiring Hit Events", + () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true) + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Statistic Not Requiring Hit Events", + () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")) + } + } + }; + } + } } } diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs index 2883e54385..a68090504d 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs @@ -3,32 +3,69 @@ using System.IO; using System.Threading; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Maintenance; namespace osu.Game.Tests.Visual.Settings { public class TestSceneMigrationScreens : ScreenTestScene { + [Cached] + private readonly NotificationOverlay notifications; + public TestSceneMigrationScreens() { - AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen())); + Children = new Drawable[] + { + notifications = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + }; + } + + [Test] + public void TestDeleteSuccess() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(true))); + } + + [Test] + public void TestDeleteFails() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen(false))); } private class TestMigrationSelectScreen : MigrationSelectScreen { - protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen()); + private readonly bool deleteSuccess; + + public TestMigrationSelectScreen(bool deleteSuccess) + { + this.deleteSuccess = deleteSuccess; + } + + protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen(deleteSuccess)); private class TestMigrationRunScreen : MigrationRunScreen { - protected override void PerformMigration() - { - Thread.Sleep(3000); - } + private readonly bool success; - public TestMigrationRunScreen() + public TestMigrationRunScreen(bool success) : base(null) { + this.success = success; + } + + protected override bool PerformMigration() + { + Thread.Sleep(3000); + return success; } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 4e46901e08..540b820250 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -41,6 +41,68 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestExternalRulesetChange() + { + createCarousel(new List()); + + AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(0), + AllowConvertedBeatmaps = true, + }, false)); + + AddStep("add mixed ruleset beatmapset", () => + { + var testMixed = TestResources.CreateTestBeatmapSetInfo(3); + + for (int i = 0; i <= 2; i++) + { + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + } + + carousel.UpdateBeatmapSet(testMixed); + }); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1; + }); + + AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(1), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1; + }); + + AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria + { + Ruleset = rulesets.AvailableRulesets.ElementAt(2), + AllowConvertedBeatmaps = true, + }, false)); + + AddUntilStep("wait for filtered difficulties", () => + { + var visibleBeatmapPanels = carousel.Items.OfType().Where(p => p.IsPresent).ToArray(); + + return visibleBeatmapPanels.Length == 2 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1 + && visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1; + }); + } + [Test] public void TestScrollPositionMaintainedOnAdd() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs new file mode 100644 index 0000000000..8b4e3f6d3a --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays.Settings; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneSafeAreaHandling : OsuGameTestScene + { + private SafeAreaDefiningContainer safeAreaContainer; + + private static BindableSafeArea safeArea; + + private readonly Bindable safeAreaPaddingTop = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingBottom = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 }; + private readonly Bindable safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Usually this would be placed between the host and the game, but that's a bit of a pain to do with the test scene hierarchy. + + // Add is required for the container to get a size (and give out correct metrics to the usages in SafeAreaContainer). + Add(safeAreaContainer = new SafeAreaDefiningContainer(safeArea = new BindableSafeArea()) + { + RelativeSizeAxes = Axes.Both + }); + + // Cache is required for the test game to see the safe area. + Dependencies.CacheAs(safeAreaContainer); + } + + public override void SetUpSteps() + { + AddStep("Add adjust controls", () => + { + Add(new Container + { + Depth = float.MinValue, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.8f, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Width = 400, + Children = new Drawable[] + { + new SettingsSlider + { + Current = safeAreaPaddingTop, + LabelText = "Top" + }, + new SettingsSlider + { + Current = safeAreaPaddingBottom, + LabelText = "Bottom" + }, + new SettingsSlider + { + Current = safeAreaPaddingLeft, + LabelText = "Left" + }, + new SettingsSlider + { + Current = safeAreaPaddingRight, + LabelText = "Right" + }, + } + } + } + }); + + safeAreaPaddingTop.BindValueChanged(_ => updateSafeArea()); + safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea()); + safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea()); + safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea()); + }); + + base.SetUpSteps(); + } + + private void updateSafeArea() + { + safeArea.Value = new MarginPadding + { + Top = safeAreaPaddingTop.Value, + Bottom = safeAreaPaddingBottom.Value, + Left = safeAreaPaddingLeft.Value, + Right = safeAreaPaddingRight.Value, + }; + } + + [Test] + public void TestSafeArea() + { + } + } +} diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 347d368a04..b4859d0c91 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tournament.IO public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty); - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { // this migration only happens once on moving to the per-tournament storage system. // listed files are those known at that point in time. @@ -94,6 +94,8 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(newStorage); storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); + + return true; } private void moveFileIfExists(string file, DirectoryInfo destination) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e4fdb3d471..633eb8f15e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -73,7 +73,9 @@ namespace osu.Game.Beatmaps new BeatmapModelManager(realm, storage, onlineLookupQueue); /// - /// Create a new . + /// Create a new beatmap set, backed by a model, + /// with a single difficulty which is backed by a model + /// and represented by the returned usable . /// public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user) { @@ -105,6 +107,40 @@ namespace osu.Game.Beatmaps return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First())); } + /// + /// Add a new difficulty to the beatmap set represented by the provided . + /// The new difficulty will be backed by a model + /// and represented by the returned . + /// + public virtual WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + { + // fetch one of the existing difficulties to copy timing points and metadata from, + // so that the user doesn't have to fill all of that out again. + // this silently assumes that all difficulties have the same timing points and metadata, + // but cases where this isn't true seem rather rare / pathological. + var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First()); + + var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone()); + + // populate circular beatmap set info <-> beatmap info references manually. + // several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()` + // rely on them being freely traversable in both directions for correct operation. + beatmapSetInfo.Beatmaps.Add(newBeatmapInfo); + newBeatmapInfo.BeatmapSet = beatmapSetInfo; + + var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; + foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints) + newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + + beatmapModelManager.Save(newBeatmapInfo, newBeatmap); + + workingBeatmapCache.Invalidate(beatmapSetInfo); + return GetWorkingBeatmap(newBeatmap.BeatmapInfo); + } + + // TODO: add back support for making a copy of another difficulty + // (likely via a separate `CopyDifficulty()` method). + /// /// Delete a beatmap difficulty. /// diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index f6666a6ea9..3a24c4808f 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Models; using osu.Game.Users; +using osu.Game.Utils; using Realms; #nullable enable @@ -16,7 +17,7 @@ namespace osu.Game.Beatmaps [ExcludeFromDynamicCompile] [Serializable] [MapTo("BeatmapMetadata")] - public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo + public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo, IDeepCloneable { public string Title { get; set; } = string.Empty; @@ -57,5 +58,18 @@ namespace osu.Game.Beatmaps IUser IBeatmapMetadataInfo.Author => Author; public override string ToString() => this.GetDisplayTitle(); + + public BeatmapMetadata DeepClone() => new BeatmapMetadata(Author.DeepClone()) + { + Title = Title, + TitleUnicode = TitleUnicode, + Artist = Artist, + ArtistUnicode = ArtistUnicode, + Source = Source, + Tags = Tags, + PreviewTime = PreviewTime, + AudioFile = AudioFile, + BackgroundFile = BackgroundFile + }; } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index e8104f2ecb..4c680bbcc9 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -46,10 +46,9 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) + public 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`. @@ -72,6 +71,12 @@ namespace osu.Game.Beatmaps // 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 = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + string targetFilename = getFilename(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); @@ -103,9 +108,9 @@ namespace osu.Game.Beatmaps public void Update(BeatmapSetInfo item) { - Realm.Write(realm => + Realm.Write(r => { - var existing = realm.Find(item.ID); + var existing = r.Find(item.ID); item.CopyChangesToRealm(existing); }); } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs index 7753d8480a..eeb86f4702 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardDifficultyList.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards bool firstGroup = true; - foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) + foreach (var group in beatmapSetInfo.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { if (!firstGroup) { diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 5b211084ab..5b467d67e2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -62,10 +62,8 @@ namespace osu.Game.Beatmaps.Drawables // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 bool collapsed = beatmapSet.Beatmaps.Count() > 12; - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key)) - { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key, rulesetGrouping, collapsed)); - } + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); } protected override void LoadComplete() diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 7a0ca2c85a..f89bbbe19d 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -58,7 +58,16 @@ namespace osu.Game.Database if (existing != null) copyChangesToRealm(beatmap, existing); else - d.Beatmaps.Add(beatmap); + { + var newBeatmap = new BeatmapInfo + { + ID = beatmap.ID, + BeatmapSet = d, + Ruleset = d.Realm.Find(beatmap.Ruleset.ShortName) + }; + d.Beatmaps.Add(newBeatmap); + copyChangesToRealm(beatmap, newBeatmap); + } } }); diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index d2b1e5e523..0d543bdbc8 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers private Bindable posX; private Bindable posY; + private Bindable safeAreaPadding; + private readonly ScalingMode? targetMode; private Bindable scalingMode; @@ -50,7 +52,7 @@ namespace osu.Game.Graphics.Containers return; allowScaling = value; - if (IsLoaded) updateSize(); + if (IsLoaded) Scheduler.AddOnce(updateSize); } } @@ -102,22 +104,25 @@ namespace osu.Game.Graphics.Containers } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, ISafeArea safeArea) { scalingMode = config.GetBindable(OsuSetting.Scaling); - scalingMode.ValueChanged += _ => updateSize(); + scalingMode.ValueChanged += _ => Scheduler.AddOnce(updateSize); sizeX = config.GetBindable(OsuSetting.ScalingSizeX); - sizeX.ValueChanged += _ => updateSize(); + sizeX.ValueChanged += _ => Scheduler.AddOnce(updateSize); sizeY = config.GetBindable(OsuSetting.ScalingSizeY); - sizeY.ValueChanged += _ => updateSize(); + sizeY.ValueChanged += _ => Scheduler.AddOnce(updateSize); posX = config.GetBindable(OsuSetting.ScalingPositionX); - posX.ValueChanged += _ => updateSize(); + posX.ValueChanged += _ => Scheduler.AddOnce(updateSize); posY = config.GetBindable(OsuSetting.ScalingPositionY); - posY.ValueChanged += _ => updateSize(); + posY.ValueChanged += _ => Scheduler.AddOnce(updateSize); + + safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy(); + safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize)); } protected override void LoadComplete() @@ -161,7 +166,10 @@ namespace osu.Game.Graphics.Containers var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; - bool requiresMasking = scaling && targetSize != Vector2.One; + bool requiresMasking = (scaling && targetSize != Vector2.One) + // For the top level scaling container, for now we apply masking if safe areas are in use. + // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. + || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero); if (requiresMasking) sizableContainer.Masking = true; diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 4267b82bb7..4ecc543ffd 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -117,6 +117,7 @@ namespace osu.Game.Graphics.UserInterface { NormalText = new OsuSpriteText { + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: text_size), @@ -124,7 +125,7 @@ namespace osu.Game.Graphics.UserInterface }, BoldText = new OsuSpriteText { - AlwaysPresent = true, + AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text. Alpha = 0, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 1b76725b04..e478144294 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -33,7 +33,8 @@ namespace osu.Game.IO /// A general purpose migration method to move the storage to a different location. /// The target storage of the migration. /// - public virtual void Migrate(Storage newStorage) + /// Whether cleanup could complete. + public virtual bool Migrate(Storage newStorage) { var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newStorage.GetFullPath(".")); @@ -57,17 +58,20 @@ namespace osu.Game.IO CopyRecursive(source, destination); ChangeTargetStorage(newStorage); - DeleteRecursive(source); + + return DeleteRecursive(source); } - protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + protected bool DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { + bool allFilesDeleted = true; + foreach (System.IO.FileInfo fi in target.GetFiles()) { if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; - AttemptOperation(() => fi.Delete()); + allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -75,11 +79,13 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; - AttemptOperation(() => dir.Delete(true)); + allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - AttemptOperation(target.Delete); + allFilesDeleted &= AttemptOperation(target.Delete, throwOnFailure: false); + + return allFilesDeleted; } protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) @@ -110,19 +116,25 @@ namespace osu.Game.IO /// /// The action to perform. /// The number of attempts (250ms wait between each). - protected static void AttemptOperation(Action action, int attempts = 10) + /// Whether to throw an exception on failure. If false, will silently fail. + protected static bool AttemptOperation(Action action, int attempts = 10, bool throwOnFailure = true) { while (true) { try { action(); - return; + return true; } catch (Exception) { if (attempts-- == 0) - throw; + { + if (throwOnFailure) + throw; + + return false; + } } Thread.Sleep(250); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 802c71e363..6e7cb545e3 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -113,11 +113,14 @@ namespace osu.Game.IO } } - public override void Migrate(Storage newStorage) + public override bool Migrate(Storage newStorage) { - base.Migrate(newStorage); + bool cleanupSucceeded = base.Migrate(newStorage); + storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); storageConfig.Save(); + + return cleanupSucceeded; } } diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs index 5fccff597c..18c849cf0a 100644 --- a/osu.Game/Models/RealmUser.cs +++ b/osu.Game/Models/RealmUser.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Database; using osu.Game.Users; +using osu.Game.Utils; using Realms; namespace osu.Game.Models { - public class RealmUser : EmbeddedObject, IUser, IEquatable + public class RealmUser : EmbeddedObject, IUser, IEquatable, IDeepCloneable { public int OnlineID { get; set; } = 1; @@ -22,5 +24,7 @@ namespace osu.Game.Models return OnlineID == other.OnlineID && Username == other.Username; } + + public RealmUser DeepClone() => (RealmUser)this.Detach().MemberwiseClone(); } } diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index 653abf7427..5d39799f6b 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -39,6 +39,7 @@ namespace osu.Game.Online.API.Requests { Best, Firsts, - Recent + Recent, + Pinned } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ebbac0dcab..dca60e54cb 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses public string MD5Hash => Checksum; - public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID }; + public IRulesetInfo Ruleset => new APIRuleset { OnlineID = RulesetID }; [JsonIgnore] public string Hash => throw new NotImplementedException(); @@ -106,5 +106,29 @@ namespace osu.Game.Online.API.Requests.Responses #endregion public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b); + + private class APIRuleset : IRulesetInfo + { + public int OnlineID { get; set; } = -1; + + public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})"; + public string ShortName => nameof(APIRuleset); + public string InstantiationInfo => string.Empty; + + public Ruleset CreateInstance() => throw new NotImplementedException(); + + public bool Equals(IRulesetInfo? other) => other is APIRuleset r && this.MatchesOnlineID(r); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is APIRuleset ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(APIRuleset)}.", nameof(other)); + + return OnlineID.CompareTo(ruleset.OnlineID); + } + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index e4a432b074..2b64e5de06 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -151,6 +151,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"scores_recent_count")] public int ScoresRecentCount; + [JsonProperty(@"scores_pinned_count")] + public int ScoresPinnedCount; + [JsonProperty(@"beatmap_playcounts_count")] public int BeatmapPlayCountsCount; diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index e24d113822..39193be1af 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -1,46 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; -using osu.Game.Online.Solo; using osu.Game.Scoring; namespace osu.Game.Online.Rooms { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : SubmitScoreRequest { - private readonly long scoreId; private readonly long roomId; private readonly long playlistItemId; - private readonly SubmittableScore score; - public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo) + public SubmitRoomScoreRequest(ScoreInfo scoreInfo, long scoreId, long roomId, long playlistItemId) + : base(scoreInfo, scoreId) { - this.scoreId = scoreId; this.roomId = roomId; this.playlistItemId = playlistItemId; - score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}"; + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{ScoreId}"; } } diff --git a/osu.Game/Online/Rooms/SubmitScoreRequest.cs b/osu.Game/Online/Rooms/SubmitScoreRequest.cs new file mode 100644 index 0000000000..b263262d2b --- /dev/null +++ b/osu.Game/Online/Rooms/SubmitScoreRequest.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Online.API; +using osu.Game.Online.Solo; +using osu.Game.Scoring; + +namespace osu.Game.Online.Rooms +{ + public abstract class SubmitScoreRequest : APIRequest + { + public readonly SubmittableScore Score; + + protected readonly long ScoreId; + + protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) + { + Score = new SubmittableScore(scoreInfo); + ScoreId = scoreId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.ContentType = "application/json"; + req.Method = HttpMethod.Put; + req.Timeout = 30000; + + req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + })); + + return req; + } + } +} diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 78ebddb2e6..77fd7b813b 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -1,46 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Net.Http; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; namespace osu.Game.Online.Solo { - public class SubmitSoloScoreRequest : APIRequest + public class SubmitSoloScoreRequest : SubmitScoreRequest { - public readonly SubmittableScore Score; - - private readonly long scoreId; - private readonly int beatmapId; - public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo) + public SubmitSoloScoreRequest(ScoreInfo scoreInfo, long scoreId, int beatmapId) + : base(scoreInfo, scoreId) { this.beatmapId = beatmapId; - this.scoreId = scoreId; - Score = new SubmittableScore(scoreInfo); } - protected override WebRequest CreateWebRequest() - { - var req = base.CreateWebRequest(); - - req.ContentType = "application/json"; - req.Method = HttpMethod.Put; - req.Timeout = 30000; - - req.AddRaw(JsonConvert.SerializeObject(Score, new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - })); - - return req; - } - - protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}"; + protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{ScoreId}"; } } diff --git a/osu.Game/Online/Spectator/SpectatedUserState.cs b/osu.Game/Online/Spectator/SpectatedUserState.cs new file mode 100644 index 0000000000..0f0a3068b8 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatedUserState.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Spectator +{ + public enum SpectatedUserState + { + /// + /// The spectated user is not yet playing. + /// + Idle, + + /// + /// The spectated user is currently playing. + /// + Playing, + + /// + /// The spectated user is currently paused. Unused for the time being. + /// + Paused, + + /// + /// The spectated user has passed gameplay. + /// + Passed, + + /// + /// The spectated user has failed gameplay. + /// + Failed, + + /// + /// The spectated user has quit gameplay. + /// + Quit + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 67aa75727d..a54ea0d9ee 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -35,19 +35,28 @@ namespace osu.Game.Online.Spectator /// public abstract IBindable IsConnected { get; } - private readonly List watchingUsers = new List(); + /// + /// The states of all users currently being watched. + /// + public IBindableDictionary WatchedUserStates => watchedUserStates; + /// + /// A global list of all players currently playing. + /// public IBindableList PlayingUsers => playingUsers; - private readonly BindableList playingUsers = new BindableList(); - public IBindableDictionary PlayingUserStates => playingUserStates; - private readonly BindableDictionary playingUserStates = new BindableDictionary(); + /// + /// All users currently being watched. + /// + private readonly List watchedUsers = new List(); + + private readonly BindableDictionary watchedUserStates = new BindableDictionary(); + private readonly BindableList playingUsers = new BindableList(); + private readonly SpectatorState currentState = new SpectatorState(); private IBeatmap? currentBeatmap; private Score? currentScore; - private readonly SpectatorState currentState = new SpectatorState(); - /// /// Whether the local user is playing. /// @@ -76,8 +85,8 @@ namespace osu.Game.Online.Spectator if (connected.NewValue) { // get all the users that were previously being watched - int[] users = watchingUsers.ToArray(); - watchingUsers.Clear(); + int[] users = watchedUsers.ToArray(); + watchedUsers.Clear(); // resubscribe to watched users. foreach (int userId in users) @@ -90,7 +99,7 @@ namespace osu.Game.Online.Spectator else { playingUsers.Clear(); - playingUserStates.Clear(); + watchedUserStates.Clear(); } }), true); } @@ -102,11 +111,8 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - // UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched. - // This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29). - // We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations. - if (watchingUsers.Contains(userId)) - playingUserStates[userId] = state; + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); }); @@ -119,7 +125,9 @@ namespace osu.Game.Online.Spectator Schedule(() => { playingUsers.Remove(userId); - playingUserStates.Remove(userId); + + if (watchedUsers.Contains(userId)) + watchedUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); }); @@ -151,6 +159,7 @@ namespace osu.Game.Online.Spectator currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); + currentState.State = SpectatedUserState.Playing; currentBeatmap = state.Beatmap; currentScore = score; @@ -161,7 +170,7 @@ namespace osu.Game.Online.Spectator public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data); - public void EndPlaying() + public void EndPlaying(GameplayState state) { // This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue). // We probably need to find a better way to handle this... @@ -176,6 +185,13 @@ namespace osu.Game.Online.Spectator IsPlaying = false; currentBeatmap = null; + if (state.HasPassed) + currentState.State = SpectatedUserState.Passed; + else if (state.HasFailed) + currentState.State = SpectatedUserState.Failed; + else + currentState.State = SpectatedUserState.Quit; + EndPlayingInternal(currentState); }); } @@ -184,10 +200,10 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchingUsers.Contains(userId)) + if (watchedUsers.Contains(userId)) return; - watchingUsers.Add(userId); + watchedUsers.Add(userId); WatchUserInternal(userId); } @@ -198,8 +214,8 @@ namespace osu.Game.Online.Spectator // Todo: This should not be a thing, but requires framework changes. Schedule(() => { - watchingUsers.Remove(userId); - playingUserStates.Remove(userId); + watchedUsers.Remove(userId); + watchedUserStates.Remove(userId); StopWatchingUserInternal(userId); }); } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index ebb91e4dd2..77686d12da 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -24,14 +24,17 @@ namespace osu.Game.Online.Spectator [Key(2)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(3)] + public SpectatedUserState State { get; set; } + public bool Equals(SpectatorState other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; + return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && State == other.State; } - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID} State:{State}"; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1713e73905..0b2644d5ba 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -89,6 +89,12 @@ namespace osu.Game } } + /// + /// The that the game should be drawn over at a top level. + /// Defaults to . + /// + protected virtual Edges SafeAreaOverrideEdges => Edges.None; + protected OsuConfigManager LocalConfig { get; private set; } protected SessionStatics SessionStatics { get; private set; } @@ -299,16 +305,23 @@ namespace osu.Game GlobalActionContainer globalBindings; - var mainContent = new Drawable[] + base.Content.Add(new SafeAreaContainer { - MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - globalBindings = new GlobalActionContainer(this) - }; - - MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }; - - base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); + SafeAreaOverrideEdges = SafeAreaOverrideEdges, + RelativeSizeAxes = Axes.Both, + Child = CreateScalingContainer().WithChildren(new Drawable[] + { + (MenuCursorContainer = new MenuCursorContainer + { + RelativeSizeAxes = Axes.Both + }).WithChild(content = new OsuTooltipContainer(MenuCursorContainer.Cursor) + { + RelativeSizeAxes = Axes.Both + }), + // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. + globalBindings = new GlobalActionContainer(this) + }) + }); KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); @@ -400,7 +413,7 @@ namespace osu.Game Scheduler.AddDelayed(GracefullyExit, 2000); } - public void Migrate(string path) + public bool Migrate(string path) { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); @@ -419,14 +432,15 @@ namespace osu.Game readyToRun.Wait(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + + Logger.Log(@"Migration complete!"); + return cleanupSucceded != false; } finally { realmBlocker?.Dispose(); } - - Logger.Log(@"Migration complete!"); } protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index fde20575fc..117de88166 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -52,21 +53,24 @@ namespace osu.Game.Overlays.Dashboard base.LoadComplete(); playingUsers.BindTo(spectatorClient.PlayingUsers); - playingUsers.BindCollectionChanged(onUsersChanged, true); + playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } - private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (int id in e.NewItems.OfType().ToArray()) + Debug.Assert(e.NewItems != null); + + foreach (int userId in e.NewItems) { - users.GetUserAsync(id).ContinueWith(task => + users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); - if (user == null) return; + if (user == null) + return; Schedule(() => { @@ -82,12 +86,10 @@ namespace osu.Game.Overlays.Dashboard break; case NotifyCollectionChangedAction.Remove: - foreach (int u in e.OldItems.OfType()) - userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); - break; + Debug.Assert(e.OldItems != null); - case NotifyCollectionChangedAction.Reset: - userFlow.Clear(); + foreach (int userId in e.OldItems) + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } }); diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 59ade0918d..ce816f84f0 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Music filter.Search.OnCommit += (sender, newText) => { - list.FirstVisibleSet.PerformRead(set => + list.FirstVisibleSet?.PerformRead(set => { BeatmapInfo toSelect = set.Beatmaps.FirstOrDefault(); diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 5532e35cc5..5c67da1911 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -46,6 +46,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks case ScoreType.Recent: return user.ScoresRecentCount; + case ScoreType.Pinned: + return user.ScoresPinnedCount; + default: return 0; } diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 00a68d5bf9..f48e33dc12 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { + new PaginatedScoreContainer(ScoreType.Pinned, User, UsersStrings.ShowExtraTopRanksPinnedTitle), new PaginatedScoreContainer(ScoreType.Best, User, UsersStrings.ShowExtraTopRanksBestTitle), new PaginatedScoreContainer(ScoreType.Firsts, User, UsersStrings.ShowExtraTopRanksFirstTitle) }; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index b0b61554eb..fb7ff0dbd1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -4,13 +4,16 @@ using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osuTK; @@ -23,6 +26,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } + [Resolved] + private NotificationOverlay notifications { get; set; } + + [Resolved] + private Storage storage { get; set; } + + [Resolved] + private GameHost host { get; set; } + public override bool AllowBackButton => false; public override bool AllowExternalScreenChange => false; @@ -84,17 +96,33 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; + var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host); + migrationTask = Task.Run(PerformMigration) - .ContinueWith(t => + .ContinueWith(task => { - if (t.IsFaulted) - Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error); + if (task.IsFaulted) + { + Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}"); + } + else if (!task.GetResultSafely()) + { + notifications.Post(new SimpleNotification + { + Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.", + Activated = () => + { + originalStorage.PresentExternally(); + return true; + } + }); + } Schedule(this.Exit); }); } - protected virtual void PerformMigration() => game?.Migrate(destination.FullName); + protected virtual bool PerformMigration() => game?.Migrate(destination.FullName) != false; public override void OnEntering(IScreen last) { diff --git a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs index 025b38257c..e8c4c71913 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using Newtonsoft.Json; namespace osu.Game.Rulesets.Difficulty @@ -12,5 +13,15 @@ namespace osu.Game.Rulesets.Difficulty /// [JsonProperty("pp")] public double Total { get; set; } + + /// + /// Return a for each attribute so that a performance breakdown can be displayed. + /// Some attributes may be omitted if they are not meant for display. + /// + /// + public virtual IEnumerable GetAttributesForDisplay() + { + yield return new PerformanceDisplayAttribute(nameof(Total), "Achieved PP", Total); + } } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs new file mode 100644 index 0000000000..273d8613c5 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdown.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Data for generating a performance breakdown by comparing performance to a perfect play. + /// + public class PerformanceBreakdown + { + /// + /// Actual gameplay performance. + /// + public PerformanceAttributes Performance { get; set; } + + /// + /// Performance of a perfect play for comparison. + /// + public PerformanceAttributes PerfectPerformance { get; set; } + } +} diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs new file mode 100644 index 0000000000..3d384f5914 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Difficulty +{ + public class PerformanceBreakdownCalculator + { + private readonly IBeatmap playableBeatmap; + private readonly BeatmapDifficultyCache difficultyCache; + private readonly ScorePerformanceCache performanceCache; + + public PerformanceBreakdownCalculator(IBeatmap playableBeatmap, BeatmapDifficultyCache difficultyCache, ScorePerformanceCache performanceCache) + { + this.playableBeatmap = playableBeatmap; + this.difficultyCache = difficultyCache; + this.performanceCache = performanceCache; + } + + [ItemCanBeNull] + public async Task CalculateAsync(ScoreInfo score, CancellationToken cancellationToken = default) + { + PerformanceAttributes[] performanceArray = await Task.WhenAll( + // compute actual performance + performanceCache.CalculatePerformanceAsync(score, cancellationToken), + // compute performance for perfect play + getPerfectPerformance(score, cancellationToken) + ).ConfigureAwait(false); + + return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] }; + } + + [ItemCanBeNull] + private Task getPerfectPerformance(ScoreInfo score, CancellationToken cancellationToken = default) + { + return Task.Run(async () => + { + Ruleset ruleset = score.Ruleset.CreateInstance(); + ScoreInfo perfectPlay = score.DeepClone(); + perfectPlay.Accuracy = 1; + perfectPlay.Passed = true; + + // calculate max combo + // todo: Get max combo from difficulty calculator instead when diffcalc properly supports lazer-first scores + perfectPlay.MaxCombo = calculateMaxCombo(playableBeatmap); + + // create statistics assuming all hit objects have perfect hit result + var statistics = playableBeatmap.HitObjects + .SelectMany(getPerfectHitResults) + .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) + .ToDictionary(pair => pair.hitResult, pair => pair.count); + perfectPlay.Statistics = statistics; + + // calculate total score + ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); + scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo; + scoreProcessor.Mods.Value = perfectPlay.Mods; + perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics); + + // compute rank achieved + // default to SS, then adjust the rank with mods + perfectPlay.Rank = ScoreRank.X; + + foreach (IApplicableToScoreProcessor mod in perfectPlay.Mods.OfType()) + { + perfectPlay.Rank = mod.AdjustRank(perfectPlay.Rank, 1); + } + + // calculate performance for this perfect score + var difficulty = await difficultyCache.GetDifficultyAsync( + playableBeatmap.BeatmapInfo, + score.Ruleset, + score.Mods, + cancellationToken + ).ConfigureAwait(false); + + // ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes + return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate(); + }, cancellationToken); + } + + private int calculateMaxCombo(IBeatmap beatmap) + { + return beatmap.HitObjects.SelectMany(getPerfectHitResults).Count(r => r.AffectsCombo()); + } + + private IEnumerable getPerfectHitResults(HitObject hitObject) + { + foreach (HitObject nested in hitObject.NestedHitObjects) + yield return nested.CreateJudgement().MaxResult; + + yield return hitObject.CreateJudgement().MaxResult; + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs new file mode 100644 index 0000000000..7958bc174e --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/PerformanceDisplayAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Data for displaying a performance attribute to user. Includes a display name for clarity. + /// + public class PerformanceDisplayAttribute + { + /// + /// Name of the attribute property in . + /// + public string PropertyName { get; } + + /// + /// A custom display name for the attribute. + /// + public string DisplayName { get; } + + /// + /// The associated attribute value. + /// + public double Value { get; } + + public PerformanceDisplayAttribute(string propertyName, string displayName, double value) + { + PropertyName = propertyName; + DisplayName = displayName; + Value = value; + } + } +} diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index ba56adac49..4174aa773c 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [Table(@"RulesetInfo")] - public sealed class EFRulesetInfo : IEquatable, IRulesetInfo + public sealed class EFRulesetInfo : IEquatable, IComparable, IRulesetInfo { public int? ID { get; set; } @@ -42,7 +42,15 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + public int CompareTo(EFRulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + + public int CompareTo(IRulesetInfo other) + { + if (!(other is EFRulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(EFRulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 44731a2495..60a02212fc 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable + public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 0a0941d1ff..88e3988431 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [MapTo("Ruleset")] - public class RulesetInfo : RealmObject, IEquatable, IRulesetInfo + public class RulesetInfo : RealmObject, IEquatable, IComparable, IRulesetInfo { [PrimaryKey] public string ShortName { get; set; } = string.Empty; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets return ShortName == other.ShortName; } - public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public bool Equals(IRulesetInfo? other) => other is RulesetInfo r && Equals(r); public int CompareTo(RulesetInfo other) { @@ -63,6 +63,14 @@ namespace osu.Game.Rulesets return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); } + public int CompareTo(IRulesetInfo other) + { + if (!(other is RulesetInfo ruleset)) + throw new ArgumentException($@"Object is not of type {nameof(RulesetInfo)}.", nameof(other)); + + return CompareTo(ruleset); + } + public override int GetHashCode() { // Importantly, ignore the underlying realm hash code, as it will usually not match. diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 976f95cef8..dcd8f12028 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI public int RecordFrameRate = 60; - [Resolved(canBeNull: true)] + [Resolved] private SpectatorClient spectatorClient { get; set; } [Resolved] @@ -48,14 +48,13 @@ namespace osu.Game.Rulesets.UI base.LoadComplete(); inputManager = GetContainingInputManager(); - - spectatorClient?.BeginPlaying(gameplayState, target); + spectatorClient.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - spectatorClient?.EndPlaying(); + spectatorClient?.EndPlaying(gameplayState); } protected override void Update() diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index b855343505..a428a66aae 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Rulesets.Difficulty; namespace osu.Game.Scoring { @@ -15,7 +16,7 @@ namespace osu.Game.Scoring /// A component which performs and acts as a central cache for performance calculations of locally databased scores. /// Currently not persisted between game sessions. /// - public class ScorePerformanceCache : MemoryCachingComponent + public class ScorePerformanceCache : MemoryCachingComponent { [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -27,10 +28,10 @@ namespace osu.Game.Scoring /// /// The score to do the calculation on. /// An optional to cancel the operation. - public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => + public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => GetAsync(new PerformanceCacheLookup(score), token); - protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) + protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) { var score = lookup.ScoreInfo; @@ -44,7 +45,7 @@ namespace osu.Game.Scoring var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Value.Attributes, score); - return calculator?.Calculate().Total; + return calculator?.Calculate(); } public readonly struct PerformanceCacheLookup diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2fead84deb..2aec63fa65 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -77,6 +77,9 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private RulesetStore rulesets { get; set; } + [Resolved] private Storage storage { get; set; } @@ -375,21 +378,34 @@ namespace osu.Game.Screens.Edit Clipboard.Content.Value = state.ClipboardContent; }); - protected void Save() + /// + /// Saves the currently edited beatmap. + /// + /// Whether the save was successful. + protected bool Save() { if (!canSave) { notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" }); - return; + return false; + } + + try + { + // save the loaded beatmap's data stream. + beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); + } + catch (Exception ex) + { + // can fail e.g. due to duplicated difficulty names. + Logger.Error(ex, ex.Message); + return false; } // no longer new after first user-triggered save. isNewBeatmap = false; - - // save the loaded beatmap's data stream. - beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); - updateLastSavedHash(); + return true; } protected override void Update() @@ -798,7 +814,7 @@ namespace osu.Game.Screens.Edit { var fileMenuItems = new List { - new EditorMenuItem("Save", MenuItemType.Standard, Save) + new EditorMenuItem("Save", MenuItemType.Standard, () => Save()) }; if (RuntimeInfo.IsDesktop) @@ -806,35 +822,51 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItemSpacer()); - var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; - - Debug.Assert(beatmapSet != null); - - var difficultyItems = new List(); - - foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset.ShortName).OrderBy(group => group.Key)) - { - if (difficultyItems.Count > 0) - difficultyItems.Add(new EditorMenuItemSpacer()); - - foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating)) - difficultyItems.Add(createDifficultyMenuItem(beatmap)); - } - - fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems }); + fileMenuItems.Add(createDifficultyCreationMenu()); + fileMenuItems.Add(createDifficultySwitchMenu()); fileMenuItems.Add(new EditorMenuItemSpacer()); fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); return fileMenuItems; } - private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo) + private EditorMenuItem createDifficultyCreationMenu() { - bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo); - return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty); + var rulesetItems = new List(); + + foreach (var ruleset in rulesets.AvailableRulesets) + rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset))); + + return new EditorMenuItem("Create new difficulty") { Items = rulesetItems }; } - protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap)); + protected void CreateNewDifficulty(RulesetInfo rulesetInfo) + => loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState()); + + private EditorMenuItem createDifficultySwitchMenu() + { + var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; + + Debug.Assert(beatmapSet != null); + + var difficultyItems = new List(); + + foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key)) + { + if (difficultyItems.Count > 0) + difficultyItems.Add(new EditorMenuItemSpacer()); + + foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating)) + { + bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap); + difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty)); + } + } + + return new EditorMenuItem("Change difficulty") { Items = difficultyItems }; + } + + protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap)); private void cancelExit() { diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 15d70e28b6..de47411fdc 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -6,10 +6,12 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -78,7 +80,26 @@ namespace osu.Game.Screens.Edit } } - public void ScheduleDifficultySwitch(BeatmapInfo nextBeatmap, EditorState editorState) + public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState) + => scheduleDifficultySwitch(() => + { + try + { + return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo); + } + catch (Exception ex) + { + // if the beatmap creation fails (e.g. due to duplicated difficulty names), + // bring the user back to the previous beatmap as a best-effort. + Logger.Error(ex, ex.Message); + return Beatmap.Value; + } + }, editorState); + + public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState) + => scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState); + + private void scheduleDifficultySwitch(Func nextBeatmap, EditorState editorState) { scheduledDifficultySwitch?.Cancel(); ValidForResume = true; @@ -87,7 +108,7 @@ namespace osu.Game.Screens.Edit scheduledDifficultySwitch = Schedule(() => { - Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap); + Beatmap.Value = nextBeatmap.Invoke(); state = editorState; // This screen is a weird exception to the rule that nothing after song select changes the global beatmap. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4bd68f2034..a397493bab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -457,6 +457,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } + // The beatmap is queried asynchronously when the selected item changes. + // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks + // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback. + // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()). + if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault)) + return; + StartPlay(); readyClickOperation?.Dispose(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 470ba59a76..772651727e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerPlayerLoader : PlayerLoader { - public bool GameplayPassed => player?.GameplayPassed == true; + public bool GameplayPassed => player?.GameplayState.HasPassed == true; private Player player; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4646f42d63..e5eeeb3448 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -208,15 +207,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { } protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { + // Allowed passed/failed users to complete their remaining replay frames. + // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. + if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed) + return; + RemoveUser(userId); var instance = instances.Single(i => i.UserId == userId); @@ -228,7 +232,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool OnBackButton() { - Debug.Assert(multiplayerClient.Room != null); + if (multiplayerClient.Room == null) + return base.OnBackButton(); // On a manual exit, set the player back to idle unless gameplay has finished. if (multiplayerClient.Room.State != MultiplayerRoomState.Open) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 83881f739d..c6a072da74 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -39,6 +39,21 @@ namespace osu.Game.Screens.Play /// public readonly Score Score; + /// + /// Whether gameplay completed without the user failing. + /// + public bool HasPassed { get; set; } + + /// + /// Whether the user failed during gameplay. + /// + public bool HasFailed { get; set; } + + /// + /// Whether the user quit gameplay without having either passed or failed. + /// + public bool HasQuit { get; set; } + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 240620b686..d4b02622d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -72,15 +72,8 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - /// - /// Whether gameplay has completed without the user having failed. - /// - public bool GameplayPassed { get; private set; } - public Action RestartRequested; - public bool HasFailed { get; private set; } - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -560,7 +553,7 @@ namespace osu.Game.Screens.Play if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && HasFailed) + if (ValidForResume && GameplayState.HasFailed) { failAnimationLayer.FinishTransforms(true); return; @@ -679,7 +672,7 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; - GameplayPassed = false; + GameplayState.HasPassed = false; ValidForResume = true; skipOutroOverlay.Hide(); return; @@ -689,7 +682,7 @@ namespace osu.Game.Screens.Play if (HealthProcessor.HasFailed) return; - GameplayPassed = true; + GameplayState.HasPassed = true; // Setting this early in the process means that even if something were to go wrong in the order of events following, there // is no chance that a user could return to the (already completed) Player instance from a child screen. @@ -805,7 +798,7 @@ namespace osu.Game.Screens.Play if (!CheckModsAllowFailure()) return false; - HasFailed = true; + GameplayState.HasFailed = true; Score.ScoreInfo.Passed = false; // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer) @@ -860,13 +853,13 @@ namespace osu.Game.Screens.Play // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state - && !HasFailed; + && !GameplayState.HasFailed; private bool canResume => // cannot resume from a non-paused state GameplayClockContainer.IsPaused.Value // cannot resume if we are already in a fail state - && !HasFailed + && !GameplayState.HasFailed // already resuming && !IsResuming; @@ -991,6 +984,9 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { + if (!GameplayState.HasPassed && !GameplayState.HasFailed) + GameplayState.HasQuit = true; + screenSuspension?.RemoveAndDisposeImmediately(); failAnimationLayer?.RemoveFilters(); @@ -1005,7 +1001,7 @@ namespace osu.Game.Screens.Play // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. - spectatorClient.EndPlaying(); + spectatorClient.EndPlaying(GameplayState); // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs index 1002e7607f..fc96dfa965 100644 --- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs +++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { Debug.Assert(Room.RoomID.Value != null); - return new SubmitRoomScoreRequest(token, Room.RoomID.Value.Value, PlaylistItem.ID, score.ScoreInfo); + return new SubmitRoomScoreRequest(score.ScoreInfo, token, Room.RoomID.Value.Value, PlaylistItem.ID); } } } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index eced2d142b..824c0072e3 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play Debug.Assert(beatmap.OnlineID > 0); - return new SubmitSoloScoreRequest(beatmap.OnlineID, token, score.ScoreInfo); + return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } } } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index b530965269..a0b07fcbd9 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) { clearDisplay(); showBeatmapPanel(spectatorState); @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); } - protected override void EndGameplay(int userId) + protected override void EndGameplay(int userId, SpectatorState state) { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index d6e4cfbe51..859b42d66d 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics else { performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely())), cancellationTokenSource.Token); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely().Total)), cancellationTokenSource.Token); } } diff --git a/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs new file mode 100644 index 0000000000..5b42554716 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/PerformanceBreakdownChart.cs @@ -0,0 +1,247 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using JetBrains.Annotations; +using osu.Framework.Allocation; +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.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Scoring; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class PerformanceBreakdownChart : Container + { + private readonly ScoreInfo score; + private readonly IBeatmap playableBeatmap; + + private Drawable spinner; + private Drawable content; + private GridContainer chart; + private OsuSpriteText achievedPerformance; + private OsuSpriteText maximumPerformance; + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + [Resolved] + private ScorePerformanceCache performanceCache { get; set; } + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } + + public PerformanceBreakdownChart(ScoreInfo score, IBeatmap playableBeatmap) + { + this.score = score; + this.playableBeatmap = playableBeatmap; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new[] + { + spinner = new LoadingSpinner(true) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre + }, + content = new FillFlowContainer + { + Alpha = 0, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.6f, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Spacing = new Vector2(15, 15), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + Width = 0.8f, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Text = "Achieved PP", + Colour = Color4Extensions.FromHex("#66FFCC") + }, + achievedPerformance = new OsuSpriteText + { + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18), + Colour = Color4Extensions.FromHex("#66FFCC") + } + }, + new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Text = "Maximum", + Colour = OsuColour.Gray(0.7f) + }, + maximumPerformance = new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 18), + Colour = OsuColour.Gray(0.7f) + } + } + } + }, + chart = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + } + } + } + } + }; + + spinner.Show(); + + new PerformanceBreakdownCalculator(playableBeatmap, difficultyCache, performanceCache) + .CalculateAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => setPerformanceValue(t.GetResultSafely()))); + } + + private void setPerformanceValue(PerformanceBreakdown breakdown) + { + spinner.Hide(); + content.FadeIn(200); + + var displayAttributes = breakdown.Performance.GetAttributesForDisplay(); + var perfectDisplayAttributes = breakdown.PerfectPerformance.GetAttributesForDisplay(); + + setTotalValues( + displayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)), + perfectDisplayAttributes.First(a => a.PropertyName == nameof(PerformanceAttributes.Total)) + ); + + var rowDimensions = new List(); + var rows = new List(); + + foreach (PerformanceDisplayAttribute attr in displayAttributes) + { + if (attr.PropertyName == nameof(PerformanceAttributes.Total)) continue; + + var row = createAttributeRow(attr, perfectDisplayAttributes.First(a => a.PropertyName == attr.PropertyName)); + + if (row != null) + { + rows.Add(row); + rowDimensions.Add(new Dimension(GridSizeMode.AutoSize)); + } + } + + chart.RowDimensions = rowDimensions.ToArray(); + chart.Content = rows.ToArray(); + } + + private void setTotalValues(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + { + achievedPerformance.Text = Math.Round(attribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(); + maximumPerformance.Text = Math.Round(perfectAttribute.Value, MidpointRounding.AwayFromZero).ToLocalisableString(); + } + + [CanBeNull] + private Drawable[] createAttributeRow(PerformanceDisplayAttribute attribute, PerformanceDisplayAttribute perfectAttribute) + { + // Don't display the attribute if its maximum is 0 + // For example, flashlight bonus would be zero if flashlight mod isn't on + if (Precision.AlmostEquals(perfectAttribute.Value, 0f)) + return null; + + float percentage = (float)(attribute.Value / perfectAttribute.Value); + + return new Drawable[] + { + new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular), + Text = attribute.DisplayName, + Colour = Colour4.White + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10, Right = 10 }, + Child = new Bar + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + CornerRadius = 2.5f, + Masking = true, + Height = 5, + BackgroundColour = Color4.White.Opacity(0.5f), + AccentColour = Color4Extensions.FromHex("#66FFCC"), + Length = percentage + } + }, + new OsuSpriteText + { + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = percentage.ToLocalisableString("0%"), + Colour = Colour4.White + } + }; + } + + protected override void Dispose(bool isDisposing) + { + cancellationTokenSource?.Cancel(); + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 485d24d024..79f813ef64 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 15 }, - Child = item.Content + Child = item.CreateContent() } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 4903983759..b43fbbdeee 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,25 +19,38 @@ namespace osu.Game.Screens.Ranking.Statistics public readonly string Name; /// - /// The content to be displayed. + /// A function returning the content to be displayed. /// - public readonly Drawable Content; + public readonly Func CreateContent; /// /// The of this row. This can be thought of as the column dimension of an encompassing . /// public readonly Dimension Dimension; + /// + /// Whether this item requires hit events. If true, will not be called if no hit events are available. + /// + public readonly bool RequiresHitEvents; + + [Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803. + public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + : this(name, () => content, true, dimension) + { + } + /// /// Creates a new , to be displayed inside a in the results screen. /// /// The name of the item. Can be to hide the item header. - /// The content to be displayed. + /// A function returning the content to be displayed. + /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + public StatisticItem([NotNull] string name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) { Name = name; - Content = content; + RequiresHitEvents = requiresHitEvents; + CreateContent = createContent; Dimension = dimension; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 567a2307dd..898bd69b2c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; @@ -74,81 +76,136 @@ namespace osu.Game.Screens.Ranking.Statistics if (newScore == null) return; - if (newScore.HitEvents.Count == 0) - { - content.Add(new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new MessagePlaceholder("Extended statistics are only available after watching a replay!"), - new ReplayDownloadButton(newScore) - { - Scale = new Vector2(1.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - }); - } - else - { - spinner.Show(); + spinner.Show(); - var localCancellationSource = loadCancellation = new CancellationTokenSource(); - IBeatmap playableBeatmap = null; + var localCancellationSource = loadCancellation = new CancellationTokenSource(); + IBeatmap playableBeatmap = null; - // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. - Task.Run(() => + // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. + Task.Run(() => + { + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); + }, loadCancellation.Token).ContinueWith(t => Schedule(() => + { + bool hitEventsAvailable = newScore.HitEvents.Count != 0; + Container container; + + var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap); + + if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents)) { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); - }, loadCancellation.Token).ContinueWith(t => Schedule(() => - { - var rows = new FillFlowContainer + container = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - Alpha = 0 + Children = new Drawable[] + { + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }; + } + else + { + FillFlowContainer rows; + container = new OsuScrollContainer(Direction.Vertical) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Children = new[] + { + rows = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(30, 15) + } + } }; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + bool anyRequiredHitEvents = false; + + foreach (var row in statisticRows) { + var columns = row.Columns; + + if (columns.Length == 0) + continue; + + var columnContent = new List(); + var dimensions = new List(); + + foreach (var col in columns) + { + if (!hitEventsAvailable && col.RequiresHitEvents) + { + anyRequiredHitEvents = true; + continue; + } + + columnContent.Add(new StatisticContainer(col) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + dimensions.Add(col.Dimension ?? new Dimension()); + } + rows.Add(new GridContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] - { - row.Columns?.Select(c => new StatisticContainer(c) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) - .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), + Content = new[] { columnContent.ToArray() }, + ColumnDimensions = dimensions.ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } - LoadComponentAsync(rows, d => + if (anyRequiredHitEvents) { - if (!Score.Value.Equals(newScore)) - return; + rows.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + new MessagePlaceholder("More statistics available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }); + } + } - spinner.Hide(); - content.Add(d); - d.FadeIn(250, Easing.OutQuint); - }, localCancellationSource.Token); - }), localCancellationSource.Token); - } + LoadComponentAsync(container, d => + { + if (!Score.Value.Equals(newScore)) + return; + + spinner.Hide(); + content.Add(d); + d.FadeIn(250, Easing.OutQuint); + }, localCancellationSource.Token); + }), localCancellationSource.Token); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f17daa8697..c3d340ac61 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -154,6 +154,7 @@ namespace osu.Game.Screens.Select private readonly DrawablePool setPool = new DrawablePool(100); private Sample spinSample; + private Sample randomSelectSample; private int visibleSetsCount; @@ -178,6 +179,7 @@ namespace osu.Game.Screens.Select private void load(OsuConfigManager config, AudioManager audio) { spinSample = audio.Samples.Get("SongSelect/random-spin"); + randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -495,6 +497,8 @@ namespace osu.Game.Screens.Select var chan = spinSample.GetChannel(); chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount); chan.Play(); + + randomSelectSample?.Play(); } private void select(CarouselItem item) @@ -921,8 +925,10 @@ namespace osu.Game.Screens.Select // child items (difficulties) are still visible. item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); - // We are applying alpha to the header here such that we can layer alpha transformations on top. - item.Header.Alpha = Math.Clamp(1.75f - 1.5f * dist, 0, 1); + // We are applying a multiplicative alpha (which is internally done by nesting an + // additional container and setting that container's alpha) such that we can + // layer alpha transformations on top. + item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } private enum PendingScrollOperation diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 533694b265..ed3aea3445 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { + public Container BorderContainer; + public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); private readonly HoverLayer hoverLayer; @@ -35,14 +37,17 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.X; Height = DrawableCarouselItem.MAX_HEIGHT; - Masking = true; - CornerRadius = corner_radius; - BorderColour = new Color4(221, 255, 255, 255); - - InternalChildren = new Drawable[] + InternalChild = BorderContainer = new Container { - Content, - hoverLayer = new HoverLayer() + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + BorderColour = new Color4(221, 255, 255, 255), + Children = new Drawable[] + { + Content, + hoverLayer = new HoverLayer() + } }; } @@ -61,21 +66,21 @@ namespace osu.Game.Screens.Select.Carousel case CarouselItemState.NotSelected: hoverLayer.InsetForBorder = false; - BorderThickness = 0; - EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = 0; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), Radius = 10, - Colour = Color4.Black.Opacity(0.5f), + Colour = Color4.Black.Opacity(100), }; break; case CarouselItemState.Selected: hoverLayer.InsetForBorder = true; - BorderThickness = border_thickness; - EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = border_thickness; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a3483aa60a..3576b77ae8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.Select.Carousel /// /// The height of a carousel beatmap, including vertical spacing. /// - public const float HEIGHT = header_height + CAROUSEL_BEATMAP_SPACING; + public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING; - private const float header_height = MAX_HEIGHT * 0.6f; + private const float height = MAX_HEIGHT * 0.6f; private readonly BeatmapInfo beatmapInfo; @@ -67,18 +67,16 @@ namespace osu.Game.Screens.Select.Carousel private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) - : base(header_height) { beatmapInfo = panel.BeatmapInfo; Item = panel; - - // Difficulty panels should start hidden for a better initial effect. - Hide(); } [BackgroundDependencyLoader(true)] private void load(BeatmapManager manager, SongSelect songSelect) { + Header.Height = height; + if (songSelect != null) { startRequested = b => songSelect.FinaliseSelection(b); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 63c004f4bc..618c5cf5ec 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -122,10 +122,12 @@ namespace osu.Game.Screens.Select.Carousel }, }; - background.DelayedLoadComplete += d => d.FadeInFromZero(750, Easing.OutQuint); - mainFlow.DelayedLoadComplete += d => d.FadeInFromZero(500, Easing.OutQuint); + background.DelayedLoadComplete += fadeContentIn; + mainFlow.DelayedLoadComplete += fadeContentIn; } + private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); + protected override void Deselected() { base.Deselected(); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 5e7ca0825a..cde3edad39 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -60,10 +60,12 @@ namespace osu.Game.Screens.Select.Carousel } } - protected DrawableCarouselItem(float headerHeight = MAX_HEIGHT) + protected DrawableCarouselItem() { RelativeSizeAxes = Axes.X; + Alpha = 0; + InternalChildren = new Drawable[] { MovementContainer = new Container @@ -71,20 +73,18 @@ namespace osu.Game.Screens.Select.Carousel RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - Header = new CarouselHeader - { - Height = headerHeight, - }, + Header = new CarouselHeader(), Content = new Container { RelativeSizeAxes = Axes.Both, - Y = headerHeight, } } }, }; } + public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha; + protected override void LoadComplete() { base.LoadComplete(); @@ -92,6 +92,12 @@ namespace osu.Game.Screens.Select.Carousel UpdateItem(); } + protected override void Update() + { + base.Update(); + Content.Y = Header.Height; + } + protected virtual void UpdateItem() { if (item == null) @@ -115,56 +121,29 @@ namespace osu.Game.Screens.Select.Carousel private void onStateChange(ValueChangedEvent _) => Scheduler.AddOnce(ApplyState); - private CarouselItemState? lastAppliedState; - protected virtual void ApplyState() { + // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. + // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. + Height = Item.TotalHeight; + Debug.Assert(Item != null); - if (lastAppliedState != Item.State.Value) + switch (Item.State.Value) { - lastAppliedState = Item.State.Value; + case CarouselItemState.NotSelected: + Deselected(); + break; - // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. - // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. - Height = Item.TotalHeight; - - switch (lastAppliedState) - { - case CarouselItemState.NotSelected: - Deselected(); - break; - - case CarouselItemState.Selected: - Selected(); - break; - } + case CarouselItemState.Selected: + Selected(); + break; } if (!Item.Visible) - Hide(); + this.FadeOut(300, Easing.OutQuint); else - Show(); - } - - private bool isVisible = true; - - public override void Show() - { - if (isVisible) - return; - - isVisible = true; - this.FadeIn(250); - } - - public override void Hide() - { - if (!isVisible) - return; - - isVisible = false; - this.FadeOut(300, Easing.OutQuint); + this.FadeIn(250); } protected virtual void Selected() diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 82523c9d9d..760915b528 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset.ShortName) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ee807762bf..f5b11448f8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -100,7 +100,6 @@ namespace osu.Game.Screens.Select private Sample sampleChangeDifficulty; private Sample sampleChangeBeatmap; - private Sample sampleRandomBeatmap; private Container carouselContainer; @@ -110,8 +109,6 @@ namespace osu.Game.Screens.Select private double audioFeedbackLastPlaybackTime; - private bool randomSelectionPending; - [Resolved] private MusicController music { get; set; } @@ -291,7 +288,6 @@ namespace osu.Game.Screens.Select sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand"); - sampleRandomBeatmap = audio.Samples.Get(@"SongSelect/select-random"); SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); if (dialogOverlay != null) @@ -319,16 +315,8 @@ namespace osu.Game.Screens.Select (new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { - NextRandom = () => - { - randomSelectionPending = true; - Carousel.SelectNextRandom(); - }, - PreviousRandom = () => - { - randomSelectionPending = true; - Carousel.SelectPreviousRandom(); - } + NextRandom = () => Carousel.SelectNextRandom(), + PreviousRandom = Carousel.SelectPreviousRandom }, null), (new FooterButtonOptions(), BeatmapOptions) }; @@ -498,9 +486,7 @@ namespace osu.Game.Screens.Select { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { - if (randomSelectionPending) - sampleRandomBeatmap.Play(); - else if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) + if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) sampleChangeDifficulty.Play(); else sampleChangeBeatmap.Play(); @@ -508,7 +494,6 @@ namespace osu.Game.Screens.Select audioFeedbackLastPlaybackTime = Time.Current; } - randomSelectionPending = false; beatmapInfoPrevious = beatmap; } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 3cf9f79611..9eb374f0f7 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Spectate [Resolved] private UserLookupCache userLookupCache { get; set; } - private readonly IBindableDictionary playingUserStates = new BindableDictionary(); + private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); @@ -77,8 +77,8 @@ namespace osu.Game.Screens.Spectate userMap[u.Id] = u; } - playingUserStates.BindTo(spectatorClient.PlayingUserStates); - playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); + userStates.BindTo(spectatorClient.WatchedUserStates); + userStates.BindCollectionChanged(onUserStatesChanged, true); realmSubscription = realm.RegisterForNotifications( realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); @@ -99,51 +99,55 @@ namespace osu.Game.Screens.Spectate { foreach ((int userId, _) in userMap) { - if (!playingUserStates.TryGetValue(userId, out var userState)) + if (!userStates.TryGetValue(userId, out var userState)) continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) - updateGameplayState(userId); + startGameplay(userId); } } - private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { case NotifyDictionaryChangedAction.Add: + case NotifyDictionaryChangedAction.Replace: foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + onUserStateChanged(userId, state); break; case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); - break; - - case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var _) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId); - - foreach ((int userId, var state) in e.NewItems.AsNonNull()) - onUserStateAdded(userId, state); + foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull()) + onUserStateRemoved(userId, state); break; } } - private void onUserStateAdded(int userId, SpectatorState state) + private void onUserStateChanged(int userId, SpectatorState newState) { - if (state.RulesetID == null || state.BeatmapID == null) + if (newState.RulesetID == null || newState.BeatmapID == null) return; if (!userMap.ContainsKey(userId)) return; - Schedule(() => OnUserStateChanged(userId, state)); - updateGameplayState(userId); + switch (newState.State) + { + case SpectatedUserState.Passed: + // Make sure that gameplay completes to the end. + if (gameplayStates.TryGetValue(userId, out var gameplayState)) + gameplayState.Score.Replay.HasReceivedAllFrames = true; + break; + + case SpectatedUserState.Playing: + Schedule(() => OnNewPlayingUserState(userId, newState)); + startGameplay(userId); + break; + } } - private void onUserStateRemoved(int userId) + private void onUserStateRemoved(int userId, SpectatorState state) { if (!userMap.ContainsKey(userId)) return; @@ -154,15 +158,15 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); - Schedule(() => EndGameplay(userId)); + Schedule(() => EndGameplay(userId, state)); } - private void updateGameplayState(int userId) + private void startGameplay(int userId) { Debug.Assert(userMap.ContainsKey(userId)); var user = userMap[userId]; - var spectatorState = playingUserStates[userId]; + var spectatorState = userStates[userId]; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance(); if (resolvedRuleset == null) @@ -191,11 +195,11 @@ namespace osu.Game.Screens.Spectate } /// - /// Invoked when a spectated user's state has changed. + /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. /// /// The user whose state has changed. /// The new state. - protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); /// /// Starts gameplay for a user. @@ -208,7 +212,8 @@ namespace osu.Game.Screens.Spectate /// Ends gameplay for a user. /// /// The user to end gameplay for. - protected abstract void EndGameplay(int userId); + /// The final user state. + protected abstract void EndGameplay(int userId, SpectatorState state); /// /// Stops spectating a user. @@ -216,7 +221,10 @@ namespace osu.Game.Screens.Spectate /// The user to stop spectating. protected void RemoveUser(int userId) { - onUserStateRemoved(userId); + if (!userStates.TryGetValue(userId, out var state)) + return; + + onUserStateRemoved(userId, state); users.Remove(userId); userMap.Remove(userId); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 709dd67087..e6b655589c 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index bcf169bb1e..331bf04644 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual public new void Redo() => base.Redo(); - public new void Save() => base.Save(); + public new bool Save() => base.Save(); public new void Cut() => base.Cut(); @@ -107,6 +107,8 @@ namespace osu.Game.Tests.Visual public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo); + public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo); + public new bool HasUnsavedChanges => base.HasUnsavedChanges; public TestEditor(EditorLoader loader = null) @@ -134,6 +136,12 @@ namespace osu.Game.Tests.Visual return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); } + public override WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo) + { + // don't actually care about properly creating a difficulty for this context. + return TestBeatmap; + } + private class TestWorkingBeatmapCache : WorkingBeatmapCache { private readonly TestBeatmapManager testBeatmapManager; diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 1a1d493249..1322a99ea7 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -40,11 +39,7 @@ namespace osu.Game.Tests.Visual.Spectator public TestSpectatorClient() { - OnNewFrames += (i, bundle) => - { - if (PlayingUsers.Contains(i)) - lastReceivedUserFrames[i] = bundle.Frames[^1]; - }; + OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; } /// @@ -63,16 +58,20 @@ namespace osu.Game.Tests.Visual.Spectator /// Ends play for an arbitrary user. /// /// The user to end play for. - public void EndPlay(int userId) + /// The spectator state to end play with. + public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { - if (!PlayingUsers.Contains(userId)) + if (!userBeatmapDictionary.ContainsKey(userId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = state }); + + userBeatmapDictionary.Remove(userId); } public new void Schedule(Action action) => base.Schedule(action); @@ -131,7 +130,7 @@ namespace osu.Game.Tests.Visual.Spectator protected override Task WatchUserInternal(int userId) { // When newly watching a user, the server sends the playing state immediately. - if (PlayingUsers.Contains(userId)) + if (userBeatmapDictionary.ContainsKey(userId)) sendPlayingState(userId); return Task.CompletedTask; @@ -145,6 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator { BeatmapID = userBeatmapDictionary[userId], RulesetID = 0, + State = SpectatedUserState.Playing }); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 50cef71b26..a9c0226951 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,8 +36,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 9ec0f1c0a0..5e0b264834 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,8 +60,8 @@ - - + + @@ -83,7 +83,7 @@ - + diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 2592f909ce..02968b87a7 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -40,11 +40,16 @@ NSMicrophoneUsageDescription We don't really use the microphone. UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft + + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft XSAppIconAssets Assets.xcassets/AppIcon.appiconset diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 702aef45f5..9c1795e45e 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -3,6 +3,7 @@ using System; using Foundation; +using osu.Framework.Graphics; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; @@ -18,6 +19,11 @@ namespace osu.iOS protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); + protected override Edges SafeAreaOverrideEdges => + // iOS shows a home indicator at the bottom, and adds a safe area to account for this. + // Because we have the home indicator (mostly) hidden we don't really care about drawing in this region. + Edges.Bottom; + private class IOSBatteryInfo : BatteryInfo { public override double ChargeLevel => Battery.ChargeLevel;