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
-
-
+
+
@@ -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;